@oss-autopilot/core 0.49.0 → 0.51.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 (38) hide show
  1. package/dist/cli-registry.js +44 -98
  2. package/dist/cli.bundle.cjs +43 -45
  3. package/dist/cli.bundle.cjs.map +4 -4
  4. package/dist/commands/daily.d.ts +1 -1
  5. package/dist/commands/daily.js +5 -42
  6. package/dist/commands/dashboard-server.d.ts +1 -1
  7. package/dist/commands/dashboard-server.js +19 -29
  8. package/dist/commands/dismiss.d.ts +1 -1
  9. package/dist/commands/dismiss.js +4 -4
  10. package/dist/commands/index.d.ts +3 -5
  11. package/dist/commands/index.js +2 -4
  12. package/dist/commands/move.d.ts +16 -0
  13. package/dist/commands/move.js +56 -0
  14. package/dist/commands/setup.d.ts +3 -0
  15. package/dist/commands/setup.js +62 -0
  16. package/dist/commands/shelve.d.ts +4 -0
  17. package/dist/commands/shelve.js +8 -2
  18. package/dist/core/category-mapping.d.ts +19 -0
  19. package/dist/core/category-mapping.js +58 -0
  20. package/dist/core/daily-logic.d.ts +1 -1
  21. package/dist/core/daily-logic.js +8 -5
  22. package/dist/core/issue-discovery.js +55 -3
  23. package/dist/core/issue-scoring.d.ts +3 -0
  24. package/dist/core/issue-scoring.js +5 -0
  25. package/dist/core/issue-vetting.js +12 -0
  26. package/dist/core/pr-monitor.d.ts +2 -27
  27. package/dist/core/pr-monitor.js +4 -110
  28. package/dist/core/state.d.ts +8 -40
  29. package/dist/core/state.js +36 -93
  30. package/dist/core/status-determination.d.ts +35 -0
  31. package/dist/core/status-determination.js +112 -0
  32. package/dist/core/types.d.ts +18 -12
  33. package/dist/core/types.js +11 -1
  34. package/package.json +1 -1
  35. package/dist/commands/override.d.ts +0 -21
  36. package/dist/commands/override.js +0 -35
  37. package/dist/commands/snooze.d.ts +0 -24
  38. package/dist/commands/snooze.js +0 -40
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Status determination logic for PRs — extracted from PRMonitor (#263).
3
+ *
4
+ * Computes the top-level status (needs_addressing vs waiting_on_maintainer),
5
+ * granular action/wait reasons, and staleness tier for a single PR based on
6
+ * its CI, review, merge-conflict, and timeline signals.
7
+ */
8
+ /**
9
+ * CI-fix bots that push commits as a direct result of the contributor's push (#568).
10
+ * Their commits represent contributor work and should count as addressing feedback.
11
+ * This is intentionally an allowlist — not all `[bot]` accounts are CI-fix bots
12
+ * (e.g. dependabot[bot] and renovate[bot] open their own PRs).
13
+ * Values must be lowercase — lookup uses .toLowerCase() for case-insensitive matching.
14
+ */
15
+ export const CI_FIX_BOTS = new Set(['autofix-ci[bot]', 'prettier-ci[bot]', 'pre-commit-ci[bot]']);
16
+ /** Minimum gap (ms) between maintainer comment and contributor commit for
17
+ * the commit to count as "addressing" the feedback (#547). Prevents false
18
+ * positives from race conditions, clock skew, and in-flight pushes. */
19
+ export const MIN_RESPONSE_GAP_MS = 2 * 60 * 1000; // 2 minutes
20
+ /**
21
+ * Check whether the HEAD commit was authored by the contributor (#547).
22
+ * Returns true when the author matches, when the author is a known CI-fix
23
+ * bot (#568), or when author info is unavailable (graceful degradation).
24
+ */
25
+ export function isContributorCommit(commitAuthor, contributorUsername) {
26
+ if (!commitAuthor || !contributorUsername)
27
+ return true; // degrade gracefully
28
+ const author = commitAuthor.toLowerCase();
29
+ if (CI_FIX_BOTS.has(author))
30
+ return true; // CI-fix bots act on behalf of the contributor (#568)
31
+ return author === contributorUsername.toLowerCase();
32
+ }
33
+ /**
34
+ * Check whether the contributor's commit is meaningfully after the maintainer's
35
+ * comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
36
+ */
37
+ export function isCommitAfterComment(commitDate, commentDate) {
38
+ const commitMs = new Date(commitDate).getTime();
39
+ const commentMs = new Date(commentDate).getTime();
40
+ if (Number.isNaN(commitMs) || Number.isNaN(commentMs)) {
41
+ // Fall back to simple string comparison (pre-#547 behavior)
42
+ return commitDate > commentDate;
43
+ }
44
+ return commitMs - commentMs >= MIN_RESPONSE_GAP_MS;
45
+ }
46
+ /**
47
+ * Determine the overall status of a PR based on its signals.
48
+ */
49
+ export function determineStatus(input) {
50
+ const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate: rawCommitDate, latestCommitAuthor, contributorUsername, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
51
+ // Compute staleness tier (independent of status)
52
+ let stalenessTier = 'active';
53
+ if (daysSinceActivity >= dormantThreshold)
54
+ stalenessTier = 'dormant';
55
+ else if (daysSinceActivity >= approachingThreshold)
56
+ 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;
61
+ // Priority order: needs_addressing (response/changes/ci/conflict/checklist) > waiting_on_maintainer (review/merge/addressed/ci_blocked)
62
+ 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
+ }
75
+ if (ciStatus === 'failing' && hasActionableCIFailure)
76
+ return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
77
+ // Non-actionable CI failures (infrastructure, fork, auth) don't block changes_addressed —
78
+ // the contributor can't fix them, so the relevant status is "waiting for re-review" (#502)
79
+ return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
80
+ }
81
+ return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
82
+ }
83
+ // Review requested changes but no unresponded comment.
84
+ // If the latest commit is before the review, the contributor hasn't addressed it yet.
85
+ if (reviewDecision === 'changes_requested' && latestChangesRequestedDate) {
86
+ if (!latestCommitDate || latestCommitDate < latestChangesRequestedDate) {
87
+ return { status: 'needs_addressing', actionReason: 'needs_changes', stalenessTier };
88
+ }
89
+ // Commit is after review — changes have been addressed
90
+ if (ciStatus === 'failing' && hasActionableCIFailure)
91
+ return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
92
+ // Non-actionable CI failures don't block changes_addressed (#502)
93
+ return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
94
+ }
95
+ if (ciStatus === 'failing') {
96
+ return hasActionableCIFailure
97
+ ? { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier }
98
+ : { status: 'waiting_on_maintainer', waitReason: 'ci_blocked', stalenessTier };
99
+ }
100
+ if (hasMergeConflict) {
101
+ return { status: 'needs_addressing', actionReason: 'merge_conflict', stalenessTier };
102
+ }
103
+ if (hasIncompleteChecklist) {
104
+ return { status: 'needs_addressing', actionReason: 'incomplete_checklist', stalenessTier };
105
+ }
106
+ // Approved and CI passing/unknown = waiting on maintainer to merge
107
+ if (reviewDecision === 'approved' && (ciStatus === 'passing' || ciStatus === 'unknown')) {
108
+ return { status: 'waiting_on_maintainer', waitReason: 'pending_merge', stalenessTier };
109
+ }
110
+ // Default: no actionable issues found. Covers pending CI, no reviews yet, etc.
111
+ return { status: 'waiting_on_maintainer', waitReason: 'pending_review', stalenessTier };
112
+ }
@@ -41,7 +41,7 @@ export interface RepoGroup {
41
41
  }
42
42
  /** GitHub's pull request review decision (from the reviewDecision GraphQL field). */
43
43
  export type ReviewDecision = 'approved' | 'changes_requested' | 'review_required' | 'unknown';
44
- /** Input options for `PRMonitor.determineStatus()`. */
44
+ /** Input options for `determineStatus()` (see status-determination.ts). */
45
45
  export interface DetermineStatusInput {
46
46
  ciStatus: CIStatus;
47
47
  hasMergeConflict: boolean;
@@ -61,6 +61,13 @@ export interface DetermineStatusInput {
61
61
  /** True if at least one failing CI check is classified as 'actionable'. */
62
62
  hasActionableCIFailure?: boolean;
63
63
  }
64
+ /** Result of `determineStatus()` — the PR's computed status classification. */
65
+ export interface DetermineStatusResult {
66
+ status: FetchedPRStatus;
67
+ actionReason?: ActionReason;
68
+ waitReason?: WaitReason;
69
+ stalenessTier: StalenessTier;
70
+ }
64
71
  /**
65
72
  * Granular reason why a PR needs addressing (contributor's turn).
66
73
  * Active values (produced by determineStatus): needs_response, needs_changes,
@@ -97,7 +104,7 @@ export interface FetchedPR {
97
104
  repo: string;
98
105
  number: number;
99
106
  title: string;
100
- /** Computed by `PRMonitor.determineStatus()` based on the fields below. */
107
+ /** Computed by `determineStatus()` based on the fields below. */
101
108
  status: FetchedPRStatus;
102
109
  /** Granular reason for needs_addressing status. Undefined when waiting_on_maintainer. */
103
110
  actionReason?: ActionReason;
@@ -414,12 +421,6 @@ export interface LocalRepoCache {
414
421
  /** ISO 8601 timestamp of when the scan was performed */
415
422
  cachedAt: string;
416
423
  }
417
- /** Metadata for a snoozed PR's CI failure. */
418
- export interface SnoozeInfo {
419
- reason: string;
420
- snoozedAt: string;
421
- expiresAt: string;
422
- }
423
424
  /** Filter for excluding repos below a minimum star count from PR count queries. */
424
425
  export interface StarFilter {
425
426
  minStars: number;
@@ -479,12 +480,14 @@ export interface AgentConfig {
479
480
  aiPolicyBlocklist?: string[];
480
481
  /** PR URLs manually shelved by the user. Shelved PRs are excluded from capacity and actionable issues. Auto-unshelved when maintainers engage. */
481
482
  shelvedPRUrls?: string[];
482
- /** Issue/PR URLs dismissed by the user, mapped to ISO timestamp of when dismissed. Issues with new responses after the dismiss timestamp resurface automatically. Named dismissedIssues for state backward compatibility (#416). */
483
+ /** Issue URLs dismissed by the user, mapped to ISO timestamp of when dismissed. Issues with new responses after the dismiss timestamp resurface automatically. */
483
484
  dismissedIssues?: Record<string, string>;
484
- /** PR URLs with snoozed CI failures, mapped to snooze metadata. Snoozed PRs are excluded from actionable CI failure list until expiry. */
485
- snoozedPRs?: Record<string, SnoozeInfo>;
486
485
  /** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
487
486
  statusOverrides?: Record<string, StatusOverride>;
487
+ /** Project categories the user is interested in (e.g., devtools, nonprofit). Used to prioritize search results. */
488
+ projectCategories?: ProjectCategory[];
489
+ /** GitHub organizations the user wants to prioritize in issue search. Org names only (not owner/repo). */
490
+ preferredOrgs?: string[];
488
491
  }
489
492
  /** Status of a user's comment thread on a GitHub issue. */
490
493
  export type IssueConversationStatus = 'new_response' | 'waiting' | 'acknowledged';
@@ -523,7 +526,10 @@ export type CommentedIssue = CommentedIssueWithResponse | CommentedIssueWithoutR
523
526
  export declare const DEFAULT_CONFIG: AgentConfig;
524
527
  /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v2 architecture. */
525
528
  export declare const INITIAL_STATE: AgentState;
526
- export type SearchPriority = 'merged_pr' | 'starred' | 'normal';
529
+ export declare const PROJECT_CATEGORIES: readonly ["nonprofit", "devtools", "infrastructure", "web-frameworks", "data-ml", "education"];
530
+ export type ProjectCategory = (typeof PROJECT_CATEGORIES)[number];
531
+ /** Priority tier for issue search results. Ordered: merged_pr > preferred_org > starred > normal. */
532
+ export type SearchPriority = 'merged_pr' | 'preferred_org' | 'starred' | 'normal';
527
533
  export interface IssueCandidate {
528
534
  issue: TrackedIssue;
529
535
  vettingResult: IssueVettingResult;
@@ -28,7 +28,8 @@ export const DEFAULT_CONFIG = {
28
28
  aiPolicyBlocklist: ['matplotlib/matplotlib'],
29
29
  shelvedPRUrls: [],
30
30
  dismissedIssues: {},
31
- snoozedPRs: {},
31
+ projectCategories: [],
32
+ preferredOrgs: [],
32
33
  };
33
34
  /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v2 architecture. */
34
35
  export const INITIAL_STATE = {
@@ -39,3 +40,12 @@ export const INITIAL_STATE = {
39
40
  events: [],
40
41
  lastRunAt: new Date().toISOString(),
41
42
  };
43
+ // -- Project category types --
44
+ export const PROJECT_CATEGORIES = [
45
+ 'nonprofit',
46
+ 'devtools',
47
+ 'infrastructure',
48
+ 'web-frameworks',
49
+ 'data-ml',
50
+ 'education',
51
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,21 +0,0 @@
1
- /**
2
- * Override command
3
- * Manually override a PR's status (needs_addressing ↔ waiting_on_maintainer).
4
- * Overrides auto-clear when the PR has new activity.
5
- */
6
- import type { FetchedPRStatus } from '../core/types.js';
7
- export interface OverrideOutput {
8
- url: string;
9
- status: FetchedPRStatus;
10
- }
11
- export interface ClearOverrideOutput {
12
- url: string;
13
- cleared: boolean;
14
- }
15
- export declare function runOverride(options: {
16
- prUrl: string;
17
- status: string;
18
- }): Promise<OverrideOutput>;
19
- export declare function runClearOverride(options: {
20
- prUrl: string;
21
- }): Promise<ClearOverrideOutput>;
@@ -1,35 +0,0 @@
1
- /**
2
- * Override command
3
- * Manually override a PR's status (needs_addressing ↔ waiting_on_maintainer).
4
- * Overrides auto-clear when the PR has new activity.
5
- */
6
- import { getStateManager } from '../core/index.js';
7
- import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
8
- const VALID_STATUSES = ['needs_addressing', 'waiting_on_maintainer'];
9
- export async function runOverride(options) {
10
- validateUrl(options.prUrl);
11
- validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
12
- if (!VALID_STATUSES.includes(options.status)) {
13
- throw new Error(`Invalid status "${options.status}". Must be one of: ${VALID_STATUSES.join(', ')}`);
14
- }
15
- const status = options.status;
16
- const stateManager = getStateManager();
17
- // Use current time as lastActivityAt — the CLI doesn't have cached PR data.
18
- // This means the override will auto-clear on the next daily run if the PR's
19
- // updatedAt is after this timestamp (which is the desired behavior: the override
20
- // will persist until new activity occurs on the PR).
21
- const lastActivityAt = new Date().toISOString();
22
- stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
23
- stateManager.save();
24
- return { url: options.prUrl, status };
25
- }
26
- export async function runClearOverride(options) {
27
- validateUrl(options.prUrl);
28
- validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
29
- const stateManager = getStateManager();
30
- const cleared = stateManager.clearStatusOverride(options.prUrl);
31
- if (cleared) {
32
- stateManager.save();
33
- }
34
- return { url: options.prUrl, cleared };
35
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Snooze/Unsnooze commands
3
- * Manages snoozing CI failure notifications for PRs with known upstream/infrastructure issues.
4
- * Snoozed PRs are excluded from the actionable CI failure list until the snooze expires.
5
- */
6
- export interface SnoozeOutput {
7
- snoozed: boolean;
8
- url: string;
9
- days: number;
10
- reason: string;
11
- expiresAt: string | undefined;
12
- }
13
- export interface UnsnoozeOutput {
14
- unsnoozed: boolean;
15
- url: string;
16
- }
17
- export declare function runSnooze(options: {
18
- prUrl: string;
19
- reason: string;
20
- days?: number;
21
- }): Promise<SnoozeOutput>;
22
- export declare function runUnsnooze(options: {
23
- prUrl: string;
24
- }): Promise<UnsnoozeOutput>;
@@ -1,40 +0,0 @@
1
- /**
2
- * Snooze/Unsnooze commands
3
- * Manages snoozing CI failure notifications for PRs with known upstream/infrastructure issues.
4
- * Snoozed PRs are excluded from the actionable CI failure list until the snooze expires.
5
- */
6
- import { getStateManager } from '../core/index.js';
7
- import { PR_URL_PATTERN, validateGitHubUrl, validateUrl, validateMessage } from './validation.js';
8
- const DEFAULT_SNOOZE_DAYS = 7;
9
- export async function runSnooze(options) {
10
- validateUrl(options.prUrl);
11
- validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
12
- validateMessage(options.reason);
13
- const days = options.days ?? DEFAULT_SNOOZE_DAYS;
14
- if (!Number.isFinite(days) || days <= 0) {
15
- throw new Error('Snooze duration must be a positive number of days.');
16
- }
17
- const stateManager = getStateManager();
18
- const added = stateManager.snoozePR(options.prUrl, options.reason, days);
19
- if (added) {
20
- stateManager.save();
21
- }
22
- const snoozeInfo = stateManager.getSnoozeInfo(options.prUrl);
23
- return {
24
- snoozed: added,
25
- url: options.prUrl,
26
- days,
27
- reason: options.reason,
28
- expiresAt: snoozeInfo?.expiresAt,
29
- };
30
- }
31
- export async function runUnsnooze(options) {
32
- validateUrl(options.prUrl);
33
- validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
34
- const stateManager = getStateManager();
35
- const removed = stateManager.unsnoozePR(options.prUrl);
36
- if (removed) {
37
- stateManager.save();
38
- }
39
- return { unsnoozed: removed, url: options.prUrl };
40
- }