@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
@@ -2,6 +2,12 @@
2
2
  * CI Analysis - Classification and analysis of CI check runs and combined statuses.
3
3
  * Extracted from PRMonitor to isolate CI-related logic (#263).
4
4
  */
5
+ import { getHttpStatusCode, errorMessage } from './errors.js';
6
+ import { debug, warn } from './logger.js';
7
+ /** Return a fresh unknown CI status (avoids shared mutable state between callers). */
8
+ function unknownCIStatus() {
9
+ return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
10
+ }
5
11
  /**
6
12
  * Known CI check name patterns that indicate fork limitations rather than real failures (#81).
7
13
  * These are deployment/preview services that require repo-level secrets unavailable in forks.
@@ -15,6 +21,7 @@ const FORK_LIMITATION_PATTERNS = [
15
21
  /chromatic/i,
16
22
  /percy/i,
17
23
  /cloudflare pages/i,
24
+ /\binternal\b/i,
18
25
  ];
19
26
  /**
20
27
  * Known CI check name patterns that indicate authorization gates (#81).
@@ -30,6 +37,7 @@ const INFRASTRUCTURE_PATTERNS = [
30
37
  /\bsetup\s+fail(ed|ure)?\b/i,
31
38
  /\bservice\s*unavailable/i,
32
39
  /\binfrastructure/i,
40
+ /\bblacksmith\b/i,
33
41
  ];
34
42
  /**
35
43
  * Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
@@ -117,15 +125,23 @@ export function analyzeCombinedStatus(combinedStatus) {
117
125
  const hasRealFailure = realStatuses.some((s) => s.state === 'failure' || s.state === 'error');
118
126
  const hasRealPending = realStatuses.some((s) => s.state === 'pending');
119
127
  const hasRealSuccess = realStatuses.some((s) => s.state === 'success');
120
- const effectiveCombinedState = hasRealFailure
121
- ? 'failure'
122
- : hasRealPending
123
- ? 'pending'
124
- : hasRealSuccess
125
- ? 'success'
126
- : realStatuses.length === 0
127
- ? 'success' // All statuses were auth gates; don't inherit original failure
128
- : combinedStatus.state;
128
+ let effectiveCombinedState;
129
+ if (hasRealFailure) {
130
+ effectiveCombinedState = 'failure';
131
+ }
132
+ else if (hasRealPending) {
133
+ effectiveCombinedState = 'pending';
134
+ }
135
+ else if (hasRealSuccess) {
136
+ effectiveCombinedState = 'success';
137
+ }
138
+ else if (realStatuses.length === 0) {
139
+ // All statuses were auth gates; don't inherit original failure
140
+ effectiveCombinedState = 'success';
141
+ }
142
+ else {
143
+ effectiveCombinedState = combinedStatus.state;
144
+ }
129
145
  const hasStatuses = combinedStatus.statuses.length > 0;
130
146
  // Collect failing status names from combined status API
131
147
  const failingStatusNames = [];
@@ -155,9 +171,72 @@ export function mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRunCount)
155
171
  if (hasSuccessfulChecks || effectiveCombinedState === 'success') {
156
172
  return { status: 'passing', failingCheckNames: [], failingCheckConclusions: new Map() };
157
173
  }
158
- // No checks found at all - this is common for repos without CI
174
+ // No checks found at all common for repos without CI
159
175
  if (!hasStatuses && checkRunCount === 0) {
160
- return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
176
+ return unknownCIStatus();
177
+ }
178
+ return unknownCIStatus();
179
+ }
180
+ /**
181
+ * Get CI status for a commit SHA by querying both the combined status API and check runs API.
182
+ * Returns the merged status and names of any failing checks for diagnostics.
183
+ */
184
+ export async function getCIStatus(octokit, owner, repo, sha) {
185
+ if (!sha)
186
+ return unknownCIStatus();
187
+ try {
188
+ // Fetch both combined status and check runs in parallel
189
+ const [statusResponse, checksResponse] = await Promise.all([
190
+ octokit.repos.getCombinedStatusForRef({ owner, repo, ref: sha }),
191
+ // 404 is expected for repos without check runs configured; log other errors for debugging
192
+ octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
193
+ const status = getHttpStatusCode(err);
194
+ // Rate limit errors must propagate — matches listReviewComments pattern (#481)
195
+ if (status === 429)
196
+ throw err;
197
+ if (status === 403) {
198
+ const msg = errorMessage(err).toLowerCase();
199
+ if (msg.includes('rate limit') || msg.includes('abuse detection'))
200
+ throw err;
201
+ }
202
+ if (status === 404) {
203
+ debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
204
+ }
205
+ else {
206
+ warn('pr-monitor', `Non-404 error fetching check runs for ${owner}/${repo}@${sha.slice(0, 7)}: ${status ?? err}`);
207
+ }
208
+ return null;
209
+ }),
210
+ ]);
211
+ const combinedStatus = statusResponse.data;
212
+ const allCheckRuns = checksResponse?.data?.check_runs || [];
213
+ // Deduplicate check runs by name, keeping only the most recent run per unique name.
214
+ // GitHub returns all historical runs (including re-runs), so without deduplication
215
+ // a superseded failure will incorrectly flag the PR as failing even after a re-run passes.
216
+ const latestCheckRunsByName = new Map();
217
+ for (const check of allCheckRuns) {
218
+ const existing = latestCheckRunsByName.get(check.name);
219
+ if (!existing || new Date(check.started_at ?? 0) > new Date(existing.started_at ?? 0)) {
220
+ latestCheckRunsByName.set(check.name, check);
221
+ }
222
+ }
223
+ const checkRuns = [...latestCheckRunsByName.values()];
224
+ const checkRunAnalysis = analyzeCheckRuns(checkRuns);
225
+ const combinedAnalysis = analyzeCombinedStatus(combinedStatus);
226
+ return mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRuns.length);
227
+ }
228
+ catch (error) {
229
+ const statusCode = getHttpStatusCode(error);
230
+ if (statusCode === 401 || statusCode === 403 || statusCode === 429) {
231
+ throw error;
232
+ }
233
+ else if (statusCode === 404) {
234
+ // Repo might not have CI configured, this is normal
235
+ debug('pr-monitor', `CI check 404 for ${owner}/${repo} (no CI configured)`);
236
+ }
237
+ else {
238
+ warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage(error)}`);
239
+ }
240
+ return unknownCIStatus();
161
241
  }
162
- return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
163
242
  }
@@ -52,43 +52,34 @@ export function applyStatusOverrides(prs, state) {
52
52
  if (!overrides || Object.keys(overrides).length === 0)
53
53
  return prs;
54
54
  const stateManager = getStateManager();
55
- // Snapshot keys before iteration clearStatusOverride mutates the same object
56
- const overrideUrls = new Set(Object.keys(overrides));
57
- let didAutoClear = false;
58
- const result = prs.map((pr) => {
59
- try {
60
- const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
61
- if (!override) {
62
- if (overrideUrls.has(pr.url))
63
- didAutoClear = true;
64
- return pr;
65
- }
66
- if (!VALID_OVERRIDE_STATUSES.has(override.status)) {
67
- warn('daily-logic', `Invalid override status "${override.status}" for ${pr.url} — ignoring`);
68
- return pr;
55
+ // Wrap in batch: getStatusOverride may auto-clear stale overrides via clearStatusOverride,
56
+ // which now auto-saves. Batching produces a single disk write for all auto-clears.
57
+ let result = [];
58
+ stateManager.batch(() => {
59
+ result = prs.map((pr) => {
60
+ try {
61
+ const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
62
+ if (!override) {
63
+ return pr;
64
+ }
65
+ if (!VALID_OVERRIDE_STATUSES.has(override.status)) {
66
+ warn('daily-logic', `Invalid override status "${override.status}" for ${pr.url} — ignoring`);
67
+ return pr;
68
+ }
69
+ if (override.status === pr.status)
70
+ return pr;
71
+ // Clear the contradictory reason field and set an appropriate default
72
+ if (override.status === 'waiting_on_maintainer') {
73
+ return { ...pr, status: override.status, actionReason: undefined, waitReason: 'pending_review' };
74
+ }
75
+ return { ...pr, status: override.status, waitReason: undefined, actionReason: 'needs_response' };
69
76
  }
70
- if (override.status === pr.status)
77
+ catch (err) {
78
+ warn('daily-logic', `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`);
71
79
  return pr;
72
- // Clear the contradictory reason field and set an appropriate default
73
- if (override.status === 'waiting_on_maintainer') {
74
- return { ...pr, status: override.status, actionReason: undefined, waitReason: 'pending_review' };
75
80
  }
76
- return { ...pr, status: override.status, waitReason: undefined, actionReason: 'needs_response' };
77
- }
78
- catch (err) {
79
- warn('daily-logic', `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`);
80
- return pr;
81
- }
81
+ });
82
82
  });
83
- // Persist any auto-cleared overrides so they don't resurrect on restart
84
- if (didAutoClear) {
85
- try {
86
- stateManager.save();
87
- }
88
- catch (err) {
89
- warn('daily-logic', `Failed to persist auto-cleared overrides — they may reappear on restart: ${errorMessage(err)}`);
90
- }
91
- }
92
83
  return result;
93
84
  }
94
85
  // ---------------------------------------------------------------------------
@@ -84,13 +84,33 @@ const WAIT_DISPLAY = {
84
84
  return 'CI checks are failing but no action is needed from you';
85
85
  },
86
86
  },
87
+ stale_ci_failure: {
88
+ label: '[Stale CI Failure]',
89
+ description: (pr) => `CI failing for ${pr.daysSinceActivity}+ days — likely pre-existing or non-actionable`,
90
+ },
87
91
  };
92
+ /** Convert a bracketed display label like "[CI Failing]" to a plain lowercase string like "ci failing". */
93
+ function labelToPlainText(reason) {
94
+ const label = ACTION_DISPLAY[reason]?.label;
95
+ if (!label)
96
+ return reason;
97
+ return label.replace(/[[\]]/g, '').toLowerCase();
98
+ }
88
99
  /** Compute display label and description for a FetchedPR (#79). */
89
100
  export function computeDisplayLabel(pr) {
90
101
  if (pr.status === 'needs_addressing' && pr.actionReason) {
91
102
  const entry = ACTION_DISPLAY[pr.actionReason];
92
- if (entry)
93
- return { displayLabel: entry.label, displayDescription: entry.description(pr) };
103
+ if (entry) {
104
+ let displayDescription = entry.description(pr);
105
+ // Append secondary action reasons when multiple issues exist (#675)
106
+ if (pr.actionReasons && pr.actionReasons.length > 1) {
107
+ const secondary = pr.actionReasons.filter((r) => r !== pr.actionReason).map(labelToPlainText);
108
+ if (secondary.length > 0) {
109
+ displayDescription += ` (also: ${secondary.join(', ')})`;
110
+ }
111
+ }
112
+ return { displayLabel: entry.label, displayDescription };
113
+ }
94
114
  }
95
115
  if (pr.status === 'waiting_on_maintainer' && pr.waitReason) {
96
116
  const entry = WAIT_DISPLAY[pr.waitReason];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GitHub Stats - Fetching merged/closed PR counts and repository star counts.
2
+ * GitHub Stats - Fetching merged/closed PR counts with star-based filtering.
3
3
  * Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GitHub Stats - Fetching merged/closed PR counts and repository star counts.
2
+ * GitHub Stats - Fetching merged/closed PR counts with star-based filtering.
3
3
  * Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
4
4
  */
5
5
  import { extractOwnerRepo, parseGitHubUrl, isOwnRepo } from './utils.js';
@@ -4,7 +4,8 @@
4
4
  */
5
5
  export { StateManager, getStateManager, resetStateManager, type Stats } from './state.js';
6
6
  export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
7
- export { IssueDiscovery, type IssueCandidate, type SearchPriority, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } from './issue-discovery.js';
7
+ export { IssueDiscovery } from './issue-discovery.js';
8
+ export { isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS } from './issue-filtering.js';
8
9
  export { IssueConversationMonitor } from './issue-conversation.js';
9
10
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
11
  export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
@@ -4,7 +4,8 @@
4
4
  */
5
5
  export { StateManager, getStateManager, resetStateManager } from './state.js';
6
6
  export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
7
- export { IssueDiscovery, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } from './issue-discovery.js';
7
+ export { IssueDiscovery } from './issue-discovery.js';
8
+ export { isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS } from './issue-filtering.js';
8
9
  export { IssueConversationMonitor } from './issue-conversation.js';
9
10
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
11
  export { getOctokit, checkRateLimit } from './github.js';
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * Issue Discovery — orchestrates multi-phase issue search across GitHub.
3
3
  *
4
- * Delegates filtering, scoring, and vetting to focused modules (#356):
5
- * - issue-filtering.ts — spam detection, doc-only filtering, per-repo caps
6
- * - issue-scoring.ts — viability scores, repo quality bonuses
7
- * - issue-vetting.ts individual issue checks (PRs, claims, health, guidelines)
4
+ * Delegates filtering, scoring, vetting, and search infrastructure to focused modules (#356, #621):
5
+ * - issue-filtering.ts — spam detection, doc-only filtering, per-repo caps
6
+ * - issue-scoring.ts — viability scores, repo quality bonuses
7
+ * - issue-vetting.ts vetting orchestration, recommendation + viability scoring
8
+ * - issue-eligibility.ts — PR existence, claim detection, requirements analysis
9
+ * - repo-health.ts — project health checks, contribution guidelines
10
+ * - search-phases.ts — search helpers, caching, batched repo search
8
11
  */
9
12
  import { type IssueCandidate } from './types.js';
10
- import { type ViabilityScoreParams } from './issue-scoring.js';
11
- export { isDocOnlyIssue, applyPerRepoCap, isLabelFarming, hasTemplatedTitle, detectLabelFarmingRepos, DOC_ONLY_LABELS, BEGINNER_LABELS, type GitHubSearchItem, } from './issue-filtering.js';
12
- export { calculateRepoQualityBonus, calculateViabilityScore, type ViabilityScoreParams } from './issue-scoring.js';
13
- export { type CheckResult } from './issue-vetting.js';
14
- export type { SearchPriority, IssueCandidate } from './types.js';
15
13
  export declare class IssueDiscovery {
16
14
  private octokit;
17
15
  private stateManager;
@@ -20,12 +18,6 @@ export declare class IssueDiscovery {
20
18
  /** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
21
19
  rateLimitWarning: string | null;
22
20
  constructor(githubToken: string);
23
- /**
24
- * Wrap octokit.search.issuesAndPullRequests with time-based caching.
25
- * Repeated identical queries within SEARCH_CACHE_TTL_MS return cached results
26
- * without consuming GitHub API rate limit points.
27
- */
28
- private cachedSearch;
29
21
  /**
30
22
  * Fetch the authenticated user's starred repositories from GitHub.
31
23
  * Updates the state manager with the list and timestamp.
@@ -35,11 +27,6 @@ export declare class IssueDiscovery {
35
27
  * Get starred repos, fetching from GitHub if cache is stale
36
28
  */
37
29
  getStarredReposWithRefresh(): Promise<string[]>;
38
- /**
39
- * Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
40
- * Extracts the common logic so each phase only needs to supply search results and context.
41
- */
42
- private filterVetAndScore;
43
30
  /**
44
31
  * Search for issues matching our criteria.
45
32
  * Searches in priority order: merged-PR repos first (no label filter), then starred repos,
@@ -51,34 +38,10 @@ export declare class IssueDiscovery {
51
38
  labels?: string[];
52
39
  maxResults?: number;
53
40
  }): Promise<IssueCandidate[]>;
54
- /**
55
- * Search for issues within specific repos using batched queries.
56
- *
57
- * To avoid GitHub's secondary rate limit (30 requests/minute), we batch
58
- * multiple repos into a single search query using OR syntax:
59
- * repo:owner1/repo1 OR repo:owner2/repo2 OR repo:owner3/repo3
60
- *
61
- * This reduces API calls from N (one per repo) to ceil(N/BATCH_SIZE).
62
- */
63
- private searchInRepos;
64
- /**
65
- * Split repos into batches of the specified size.
66
- */
67
- private batchRepos;
68
41
  /**
69
42
  * Vet a specific issue (delegates to IssueVetter).
70
43
  */
71
44
  vetIssue(issueUrl: string): Promise<IssueCandidate>;
72
- /**
73
- * Analyze issue requirements for clarity (delegates to IssueVetter).
74
- * Kept on class for backward compatibility.
75
- */
76
- analyzeRequirements(body: string): boolean;
77
- /**
78
- * Calculate viability score for an issue (delegates to issue-scoring module).
79
- * Kept on class for backward compatibility with tests that call instance.calculateViabilityScore().
80
- */
81
- calculateViabilityScore(params: ViabilityScoreParams): number;
82
45
  /**
83
46
  * Save search results to ~/.oss-autopilot/found-issues.md
84
47
  * Results are sorted by viability score (highest first)