@oss-autopilot/core 0.44.0 → 0.44.2
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.
- package/dist/cli-registry.d.ts +21 -0
- package/dist/cli-registry.js +935 -0
- package/dist/cli.bundle.cjs +115 -114
- package/dist/cli.bundle.cjs.map +7 -0
- package/dist/cli.d.ts +5 -6
- package/dist/cli.js +14 -809
- package/dist/commands/dashboard-server.js +24 -4
- package/dist/core/ci-analysis.d.ts +3 -17
- package/dist/core/ci-analysis.js +3 -3
- package/dist/core/display-utils.js +8 -1
- package/dist/core/github-stats.d.ts +0 -20
- package/dist/core/github-stats.js +1 -1
- package/dist/core/github.js +2 -2
- package/dist/core/http-cache.d.ts +10 -2
- package/dist/core/http-cache.js +19 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/issue-discovery.d.ts +6 -0
- package/dist/core/issue-discovery.js +18 -3
- package/dist/core/issue-vetting.js +41 -36
- package/dist/core/pr-monitor.js +11 -6
- package/dist/core/review-analysis.js +11 -1
- package/dist/core/state.js +14 -1
- package/dist/core/test-utils.d.ts +1 -3
- package/dist/core/test-utils.js +0 -41
- package/dist/core/types.d.ts +4 -2
- package/package.json +3 -3
|
@@ -10,11 +10,18 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getStateManager, getGitHubToken, getDataDir } from '../core/index.js';
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
|
-
import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN } from './validation.js';
|
|
13
|
+
import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, } from './validation.js';
|
|
14
14
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
|
|
15
15
|
import { openInBrowser } from './startup.js';
|
|
16
16
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
17
|
-
const VALID_ACTIONS = new Set([
|
|
17
|
+
const VALID_ACTIONS = new Set([
|
|
18
|
+
'shelve',
|
|
19
|
+
'unshelve',
|
|
20
|
+
'snooze',
|
|
21
|
+
'unsnooze',
|
|
22
|
+
'dismiss',
|
|
23
|
+
'undismiss',
|
|
24
|
+
]);
|
|
18
25
|
const MAX_BODY_BYTES = 10_240;
|
|
19
26
|
const MIME_TYPES = {
|
|
20
27
|
'.html': 'text/html',
|
|
@@ -125,6 +132,7 @@ function buildDashboardJson(digest, state, commentedIssues) {
|
|
|
125
132
|
monthlyClosed,
|
|
126
133
|
activePRs: digest.openPRs || [],
|
|
127
134
|
shelvedPRUrls: state.config.shelvedPRUrls || [],
|
|
135
|
+
dismissedUrls: Object.keys(state.config.dismissedIssues || {}),
|
|
128
136
|
recentlyMergedPRs: digest.recentlyMergedPRs || [],
|
|
129
137
|
recentlyClosedPRs: digest.recentlyClosedPRs || [],
|
|
130
138
|
autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
|
|
@@ -257,10 +265,14 @@ export async function startDashboardServer(options) {
|
|
|
257
265
|
sendError(res, 400, 'Missing or invalid "url" field');
|
|
258
266
|
return;
|
|
259
267
|
}
|
|
260
|
-
// Validate URL format — same checks as CLI commands
|
|
268
|
+
// Validate URL format — same checks as CLI commands.
|
|
269
|
+
// Dismiss/undismiss accepts both PR and issue URLs; other actions are PR-only.
|
|
270
|
+
const isDismissAction = body.action === 'dismiss' || body.action === 'undismiss';
|
|
271
|
+
const urlPattern = isDismissAction ? ISSUE_OR_PR_URL_PATTERN : PR_URL_PATTERN;
|
|
272
|
+
const urlType = isDismissAction ? 'issue or PR' : 'PR';
|
|
261
273
|
try {
|
|
262
274
|
validateUrl(body.url);
|
|
263
|
-
validateGitHubUrl(body.url,
|
|
275
|
+
validateGitHubUrl(body.url, urlPattern, urlType);
|
|
264
276
|
}
|
|
265
277
|
catch (err) {
|
|
266
278
|
if (err instanceof ValidationError) {
|
|
@@ -299,6 +311,7 @@ export async function startDashboardServer(options) {
|
|
|
299
311
|
switch (body.action) {
|
|
300
312
|
case 'shelve':
|
|
301
313
|
stateManager.shelvePR(body.url);
|
|
314
|
+
stateManager.undismissIssue(body.url); // prevent dual state
|
|
302
315
|
break;
|
|
303
316
|
case 'unshelve':
|
|
304
317
|
stateManager.unshelvePR(body.url);
|
|
@@ -309,6 +322,13 @@ export async function startDashboardServer(options) {
|
|
|
309
322
|
case 'unsnooze':
|
|
310
323
|
stateManager.unsnoozePR(body.url);
|
|
311
324
|
break;
|
|
325
|
+
case 'dismiss':
|
|
326
|
+
stateManager.dismissIssue(body.url, new Date().toISOString());
|
|
327
|
+
stateManager.unshelvePR(body.url); // prevent dual state
|
|
328
|
+
break;
|
|
329
|
+
case 'undismiss':
|
|
330
|
+
stateManager.undismissIssue(body.url);
|
|
331
|
+
break;
|
|
312
332
|
}
|
|
313
333
|
stateManager.save();
|
|
314
334
|
}
|
|
@@ -3,21 +3,6 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate CI-related logic (#263).
|
|
4
4
|
*/
|
|
5
5
|
import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
|
|
6
|
-
/**
|
|
7
|
-
* Known CI check name patterns that indicate fork limitations rather than real failures (#81).
|
|
8
|
-
* These are deployment/preview services that require repo-level secrets unavailable in forks.
|
|
9
|
-
*/
|
|
10
|
-
export declare const FORK_LIMITATION_PATTERNS: RegExp[];
|
|
11
|
-
/**
|
|
12
|
-
* Known CI check name patterns that indicate authorization gates (#81).
|
|
13
|
-
* These require maintainer approval and are not real failures.
|
|
14
|
-
*/
|
|
15
|
-
export declare const AUTH_GATE_PATTERNS: RegExp[];
|
|
16
|
-
/**
|
|
17
|
-
* Known CI check name patterns that indicate infrastructure/transient failures (#145).
|
|
18
|
-
* These are runner issues, dependency install problems, or service outages — not code failures.
|
|
19
|
-
*/
|
|
20
|
-
export declare const INFRASTRUCTURE_PATTERNS: RegExp[];
|
|
21
6
|
/**
|
|
22
7
|
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
|
|
23
8
|
* Default is 'actionable' — only known patterns get reclassified.
|
|
@@ -45,7 +30,7 @@ export declare function analyzeCheckRuns(checkRuns: Array<{
|
|
|
45
30
|
failingCheckConclusions: Map<string, string>;
|
|
46
31
|
};
|
|
47
32
|
/** Result shape from analyzeCheckRuns, used by mergeStatuses. */
|
|
48
|
-
|
|
33
|
+
interface CheckRunAnalysis {
|
|
49
34
|
hasFailingChecks: boolean;
|
|
50
35
|
hasPendingChecks: boolean;
|
|
51
36
|
hasSuccessfulChecks: boolean;
|
|
@@ -53,7 +38,7 @@ export interface CheckRunAnalysis {
|
|
|
53
38
|
failingCheckConclusions: Map<string, string>;
|
|
54
39
|
}
|
|
55
40
|
/** Result shape from analyzeCombinedStatus, used by mergeStatuses. */
|
|
56
|
-
|
|
41
|
+
interface CombinedStatusAnalysis {
|
|
57
42
|
effectiveCombinedState: string;
|
|
58
43
|
hasStatuses: boolean;
|
|
59
44
|
failingStatusNames: string[];
|
|
@@ -76,3 +61,4 @@ export declare function analyzeCombinedStatus(combinedStatus: {
|
|
|
76
61
|
* Priority: failing > pending > passing > unknown.
|
|
77
62
|
*/
|
|
78
63
|
export declare function mergeStatuses(checkRunAnalysis: CheckRunAnalysis, combinedAnalysis: CombinedStatusAnalysis, checkRunCount: number): CIStatusResult;
|
|
64
|
+
export {};
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Known CI check name patterns that indicate fork limitations rather than real failures (#81).
|
|
7
7
|
* These are deployment/preview services that require repo-level secrets unavailable in forks.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
const FORK_LIMITATION_PATTERNS = [
|
|
10
10
|
/vercel/i,
|
|
11
11
|
/netlify/i,
|
|
12
12
|
/\bpreview\s*deploy/i,
|
|
@@ -20,12 +20,12 @@ export const FORK_LIMITATION_PATTERNS = [
|
|
|
20
20
|
* Known CI check name patterns that indicate authorization gates (#81).
|
|
21
21
|
* These require maintainer approval and are not real failures.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
const AUTH_GATE_PATTERNS = [/authoriz/i, /approval/i, /\bcla\b/i, /license\/cla/i];
|
|
24
24
|
/**
|
|
25
25
|
* Known CI check name patterns that indicate infrastructure/transient failures (#145).
|
|
26
26
|
* These are runner issues, dependency install problems, or service outages — not code failures.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
const INFRASTRUCTURE_PATTERNS = [
|
|
29
29
|
/\binstall\s*(os\s*)?dep(endenc|s\b)/i,
|
|
30
30
|
/\bsetup\s+fail(ed|ure)?\b/i,
|
|
31
31
|
/\bservice\s*unavailable/i,
|
|
@@ -35,7 +35,14 @@ const STATUS_DISPLAY = {
|
|
|
35
35
|
},
|
|
36
36
|
ci_blocked: {
|
|
37
37
|
label: '[CI Blocked]',
|
|
38
|
-
description: () =>
|
|
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
|
+
},
|
|
39
46
|
},
|
|
40
47
|
ci_not_running: {
|
|
41
48
|
label: '[CI Not Running]',
|
|
@@ -28,26 +28,6 @@ export declare function fetchUserMergedPRCounts(octokit: Octokit, githubUsername
|
|
|
28
28
|
* Used to populate closedWithoutMergeCount in repo scores for accurate merge rate.
|
|
29
29
|
*/
|
|
30
30
|
export declare function fetchUserClosedPRCounts(octokit: Octokit, githubUsername: string): Promise<PRCountsResult<number>>;
|
|
31
|
-
/**
|
|
32
|
-
* Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
|
|
33
|
-
* Returns parsed search results that pass all filters.
|
|
34
|
-
*/
|
|
35
|
-
export declare function fetchRecentPRs<T>(octokit: Octokit, config: {
|
|
36
|
-
githubUsername: string;
|
|
37
|
-
excludeRepos: string[];
|
|
38
|
-
excludeOrgs?: string[];
|
|
39
|
-
}, query: string, label: string, days: number, mapItem: (item: {
|
|
40
|
-
html_url: string;
|
|
41
|
-
title: string;
|
|
42
|
-
closed_at: string | null;
|
|
43
|
-
pull_request?: {
|
|
44
|
-
merged_at?: string | null;
|
|
45
|
-
};
|
|
46
|
-
}, parsed: {
|
|
47
|
-
owner: string;
|
|
48
|
-
repo: string;
|
|
49
|
-
number: number;
|
|
50
|
-
}) => T): Promise<T[]>;
|
|
51
31
|
/**
|
|
52
32
|
* Fetch PRs closed without merge in the last N days.
|
|
53
33
|
* Returns lightweight ClosedPR objects for surfacing in the daily digest.
|
|
@@ -168,7 +168,7 @@ export function fetchUserClosedPRCounts(octokit, githubUsername) {
|
|
|
168
168
|
* Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
|
|
169
169
|
* Returns parsed search results that pass all filters.
|
|
170
170
|
*/
|
|
171
|
-
|
|
171
|
+
async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
|
|
172
172
|
if (!config.githubUsername) {
|
|
173
173
|
warn(MODULE, `Skipping recently ${label} PRs fetch: no githubUsername configured. Run /setup-oss to configure.`);
|
|
174
174
|
return [];
|
package/dist/core/github.js
CHANGED
|
@@ -19,7 +19,7 @@ export function getOctokit(token) {
|
|
|
19
19
|
_octokit = new ThrottledOctokit({
|
|
20
20
|
auth: token,
|
|
21
21
|
throttle: {
|
|
22
|
-
onRateLimit: (retryAfter, options,
|
|
22
|
+
onRateLimit: (retryAfter, options, _octokit, retryCount) => {
|
|
23
23
|
const opts = options;
|
|
24
24
|
const resetAt = new Date(Date.now() + retryAfter * 1000);
|
|
25
25
|
if (retryCount < 2) {
|
|
@@ -29,7 +29,7 @@ export function getOctokit(token) {
|
|
|
29
29
|
warn(MODULE, `Rate limit exceeded, not retrying — ${opts.method} ${opts.url} (resets at ${formatResetTime(resetAt)})`);
|
|
30
30
|
return false;
|
|
31
31
|
},
|
|
32
|
-
onSecondaryRateLimit: (retryAfter, options,
|
|
32
|
+
onSecondaryRateLimit: (retryAfter, options, _octokit, retryCount) => {
|
|
33
33
|
const opts = options;
|
|
34
34
|
const resetAt = new Date(Date.now() + retryAfter * 1000);
|
|
35
35
|
if (retryCount < 1) {
|
|
@@ -78,8 +78,6 @@ export declare class HttpCache {
|
|
|
78
78
|
* The singleton is lazily initialized on first access.
|
|
79
79
|
*/
|
|
80
80
|
export declare function getHttpCache(): HttpCache;
|
|
81
|
-
/** Reset the singleton (for tests). */
|
|
82
|
-
export declare function resetHttpCache(): void;
|
|
83
81
|
/**
|
|
84
82
|
* Wraps an Octokit `repos.get`-style call with ETag caching and request
|
|
85
83
|
* deduplication.
|
|
@@ -101,3 +99,13 @@ export declare function cachedRequest<T>(cache: HttpCache, url: string, fetcher:
|
|
|
101
99
|
data: T;
|
|
102
100
|
headers?: Record<string, string>;
|
|
103
101
|
}>): Promise<T>;
|
|
102
|
+
/**
|
|
103
|
+
* Time-based cache wrapper (no ETag / conditional requests).
|
|
104
|
+
*
|
|
105
|
+
* If a cached result exists and is younger than `maxAgeMs`, returns it.
|
|
106
|
+
* Otherwise calls `fetcher`, caches the result, and returns it.
|
|
107
|
+
*
|
|
108
|
+
* Use this for expensive operations whose results change slowly
|
|
109
|
+
* (e.g. search queries, project health checks).
|
|
110
|
+
*/
|
|
111
|
+
export declare function cachedTimeBased<T>(cache: HttpCache, key: string, maxAgeMs: number, fetcher: () => Promise<T>): Promise<T>;
|
package/dist/core/http-cache.js
CHANGED
|
@@ -204,10 +204,6 @@ export function getHttpCache() {
|
|
|
204
204
|
}
|
|
205
205
|
return _httpCache;
|
|
206
206
|
}
|
|
207
|
-
/** Reset the singleton (for tests). */
|
|
208
|
-
export function resetHttpCache() {
|
|
209
|
-
_httpCache = null;
|
|
210
|
-
}
|
|
211
207
|
// ---------------------------------------------------------------------------
|
|
212
208
|
// Octokit integration helpers
|
|
213
209
|
// ---------------------------------------------------------------------------
|
|
@@ -272,6 +268,25 @@ export async function cachedRequest(cache, url, fetcher) {
|
|
|
272
268
|
cleanup();
|
|
273
269
|
}
|
|
274
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Time-based cache wrapper (no ETag / conditional requests).
|
|
273
|
+
*
|
|
274
|
+
* If a cached result exists and is younger than `maxAgeMs`, returns it.
|
|
275
|
+
* Otherwise calls `fetcher`, caches the result, and returns it.
|
|
276
|
+
*
|
|
277
|
+
* Use this for expensive operations whose results change slowly
|
|
278
|
+
* (e.g. search queries, project health checks).
|
|
279
|
+
*/
|
|
280
|
+
export async function cachedTimeBased(cache, key, maxAgeMs, fetcher) {
|
|
281
|
+
const cached = cache.getIfFresh(key, maxAgeMs);
|
|
282
|
+
if (cached) {
|
|
283
|
+
debug(MODULE, `Time-based cache hit for ${key}`);
|
|
284
|
+
return cached;
|
|
285
|
+
}
|
|
286
|
+
const result = await fetcher();
|
|
287
|
+
cache.set(key, '', result);
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
275
290
|
/**
|
|
276
291
|
* Detect whether an error is a 304 Not Modified response.
|
|
277
292
|
* Octokit throws a RequestError with status 304 for conditional requests.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
|
11
11
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
|
-
export { HttpCache, getHttpCache,
|
|
14
|
+
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -11,6 +11,6 @@ export { getOctokit, checkRateLimit } from './github.js';
|
|
|
11
11
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
|
-
export { HttpCache, getHttpCache,
|
|
14
|
+
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export * from './types.js';
|
|
@@ -20,6 +20,12 @@ export declare class IssueDiscovery {
|
|
|
20
20
|
/** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
|
|
21
21
|
rateLimitWarning: string | null;
|
|
22
22
|
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;
|
|
23
29
|
/**
|
|
24
30
|
* Fetch the authenticated user's starred repositories from GitHub.
|
|
25
31
|
* Updates the state manager with the list and timestamp.
|
|
@@ -14,6 +14,7 @@ import { daysBetween, getDataDir } from './utils.js';
|
|
|
14
14
|
import { DEFAULT_CONFIG } from './types.js';
|
|
15
15
|
import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
16
16
|
import { debug, info, warn } from './logger.js';
|
|
17
|
+
import { getHttpCache, cachedTimeBased } from './http-cache.js';
|
|
17
18
|
import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
|
|
18
19
|
import { IssueVetter } from './issue-vetting.js';
|
|
19
20
|
import { calculateViabilityScore as calcViabilityScore } from './issue-scoring.js';
|
|
@@ -22,6 +23,8 @@ import { calculateViabilityScore as calcViabilityScore } from './issue-scoring.j
|
|
|
22
23
|
export { isDocOnlyIssue, applyPerRepoCap, isLabelFarming, hasTemplatedTitle, detectLabelFarmingRepos, DOC_ONLY_LABELS, BEGINNER_LABELS, } from './issue-filtering.js';
|
|
23
24
|
export { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
|
|
24
25
|
const MODULE = 'issue-discovery';
|
|
26
|
+
/** TTL for cached search API results (15 minutes). */
|
|
27
|
+
const SEARCH_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
25
28
|
export class IssueDiscovery {
|
|
26
29
|
octokit;
|
|
27
30
|
stateManager;
|
|
@@ -35,6 +38,18 @@ export class IssueDiscovery {
|
|
|
35
38
|
this.stateManager = getStateManager();
|
|
36
39
|
this.vetter = new IssueVetter(this.octokit, this.stateManager);
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Wrap octokit.search.issuesAndPullRequests with time-based caching.
|
|
43
|
+
* Repeated identical queries within SEARCH_CACHE_TTL_MS return cached results
|
|
44
|
+
* without consuming GitHub API rate limit points.
|
|
45
|
+
*/
|
|
46
|
+
async cachedSearch(params) {
|
|
47
|
+
const cacheKey = `search:${params.q}:${params.sort}:${params.order}:${params.per_page}`;
|
|
48
|
+
return cachedTimeBased(getHttpCache(), cacheKey, SEARCH_CACHE_TTL_MS, async () => {
|
|
49
|
+
const { data } = await this.octokit.search.issuesAndPullRequests(params);
|
|
50
|
+
return data;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
38
53
|
/**
|
|
39
54
|
* Fetch the authenticated user's starred repositories from GitHub.
|
|
40
55
|
* Updates the state manager with the list and timestamp.
|
|
@@ -292,7 +307,7 @@ export class IssueDiscovery {
|
|
|
292
307
|
info(MODULE, 'Phase 2: General issue search...');
|
|
293
308
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
294
309
|
try {
|
|
295
|
-
const
|
|
310
|
+
const data = await this.cachedSearch({
|
|
296
311
|
q: baseQuery,
|
|
297
312
|
sort: 'created',
|
|
298
313
|
order: 'desc',
|
|
@@ -334,7 +349,7 @@ export class IssueDiscovery {
|
|
|
334
349
|
.replace(/ +/g, ' ')
|
|
335
350
|
.trim();
|
|
336
351
|
try {
|
|
337
|
-
const
|
|
352
|
+
const data = await this.cachedSearch({
|
|
338
353
|
q: phase3Query,
|
|
339
354
|
sort: 'updated',
|
|
340
355
|
order: 'desc',
|
|
@@ -434,7 +449,7 @@ export class IssueDiscovery {
|
|
|
434
449
|
// Build repo filter: (repo:a OR repo:b OR repo:c)
|
|
435
450
|
const repoFilter = batch.map((r) => `repo:${r}`).join(' OR ');
|
|
436
451
|
const batchQuery = `${baseQuery} (${repoFilter})`;
|
|
437
|
-
const
|
|
452
|
+
const data = await this.cachedSearch({
|
|
438
453
|
q: batchQuery,
|
|
439
454
|
sort: 'created',
|
|
440
455
|
order: 'desc',
|
|
@@ -8,7 +8,7 @@ import { paginateAll } from './pagination.js';
|
|
|
8
8
|
import { parseGitHubUrl, daysBetween } from './utils.js';
|
|
9
9
|
import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
10
10
|
import { warn } from './logger.js';
|
|
11
|
-
import { getHttpCache, cachedRequest } from './http-cache.js';
|
|
11
|
+
import { getHttpCache, cachedRequest, cachedTimeBased } from './http-cache.js';
|
|
12
12
|
import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
|
|
13
13
|
const MODULE = 'issue-vetting';
|
|
14
14
|
// Concurrency limit for parallel API calls
|
|
@@ -16,6 +16,8 @@ const MAX_CONCURRENT_REQUESTS = 5;
|
|
|
16
16
|
// Cache for contribution guidelines (expires after 1 hour, max 100 entries)
|
|
17
17
|
const guidelinesCache = new Map();
|
|
18
18
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
19
|
+
/** TTL for cached project health results (4 hours). Health data (stars, commits, CI) changes slowly. */
|
|
20
|
+
const HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1000;
|
|
19
21
|
const CACHE_MAX_SIZE = 100;
|
|
20
22
|
function pruneCache() {
|
|
21
23
|
const now = Date.now();
|
|
@@ -376,47 +378,50 @@ export class IssueVetter {
|
|
|
376
378
|
}
|
|
377
379
|
}
|
|
378
380
|
async checkProjectHealth(owner, repo) {
|
|
381
|
+
const cache = getHttpCache();
|
|
382
|
+
const healthCacheKey = `health:${owner}/${repo}`;
|
|
379
383
|
try {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
owner,
|
|
387
|
-
repo,
|
|
388
|
-
per_page: 1,
|
|
389
|
-
});
|
|
390
|
-
const lastCommit = commits[0];
|
|
391
|
-
const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
|
|
392
|
-
const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
|
|
393
|
-
// Check CI status (simplified - just check if workflows exist)
|
|
394
|
-
let ciStatus = 'unknown';
|
|
395
|
-
try {
|
|
396
|
-
const { data: workflows } = await this.octokit.actions.listRepoWorkflows({
|
|
384
|
+
return await cachedTimeBased(cache, healthCacheKey, HEALTH_CACHE_TTL_MS, async () => {
|
|
385
|
+
// Get repo info (with ETag caching — repo metadata changes infrequently)
|
|
386
|
+
const url = `/repos/${owner}/${repo}`;
|
|
387
|
+
const repoData = await cachedRequest(cache, url, (headers) => this.octokit.repos.get({ owner, repo, headers }));
|
|
388
|
+
// Get recent commits
|
|
389
|
+
const { data: commits } = await this.octokit.repos.listCommits({
|
|
397
390
|
owner,
|
|
398
391
|
repo,
|
|
399
392
|
per_page: 1,
|
|
400
393
|
});
|
|
401
|
-
|
|
402
|
-
|
|
394
|
+
const lastCommit = commits[0];
|
|
395
|
+
const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
|
|
396
|
+
const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
|
|
397
|
+
// Check CI status (simplified - just check if workflows exist)
|
|
398
|
+
let ciStatus = 'unknown';
|
|
399
|
+
try {
|
|
400
|
+
const { data: workflows } = await this.octokit.actions.listRepoWorkflows({
|
|
401
|
+
owner,
|
|
402
|
+
repo,
|
|
403
|
+
per_page: 1,
|
|
404
|
+
});
|
|
405
|
+
if (workflows.total_count > 0) {
|
|
406
|
+
ciStatus = 'passing'; // Assume passing if workflows exist
|
|
407
|
+
}
|
|
403
408
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
};
|
|
409
|
+
catch (error) {
|
|
410
|
+
const errMsg = errorMessage(error);
|
|
411
|
+
warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${errMsg}. Defaulting to unknown.`);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
repo: `${owner}/${repo}`,
|
|
415
|
+
lastCommitAt,
|
|
416
|
+
daysSinceLastCommit,
|
|
417
|
+
openIssuesCount: repoData.open_issues_count,
|
|
418
|
+
avgIssueResponseDays: 0, // Would need more API calls to calculate
|
|
419
|
+
ciStatus,
|
|
420
|
+
isActive: daysSinceLastCommit < 30,
|
|
421
|
+
stargazersCount: repoData.stargazers_count,
|
|
422
|
+
forksCount: repoData.forks_count,
|
|
423
|
+
};
|
|
424
|
+
});
|
|
420
425
|
}
|
|
421
426
|
catch (error) {
|
|
422
427
|
const errMsg = errorMessage(error);
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -234,7 +234,10 @@ export class PRMonitor {
|
|
|
234
234
|
const daysSinceActivity = daysBetween(new Date(ghPR.updated_at), new Date());
|
|
235
235
|
// Find the date of the latest changes_requested review (delegated to review-analysis module)
|
|
236
236
|
const latestChangesRequestedDate = getLatestChangesRequestedDate(reviews);
|
|
237
|
+
// Classify failing checks (delegated to ci-analysis module)
|
|
238
|
+
const classifiedChecks = classifyFailingChecks(failingCheckNames, failingCheckConclusions);
|
|
237
239
|
// Determine status
|
|
240
|
+
const hasActionableCIFailure = ciStatus === 'failing' && classifiedChecks.some((c) => c.category === 'actionable');
|
|
238
241
|
const status = this.determineStatus({
|
|
239
242
|
ciStatus,
|
|
240
243
|
hasMergeConflict,
|
|
@@ -247,9 +250,8 @@ export class PRMonitor {
|
|
|
247
250
|
latestCommitDate,
|
|
248
251
|
lastMaintainerCommentDate: lastMaintainerComment?.createdAt,
|
|
249
252
|
latestChangesRequestedDate,
|
|
253
|
+
hasActionableCIFailure,
|
|
250
254
|
});
|
|
251
|
-
// Classify failing checks (delegated to ci-analysis module)
|
|
252
|
-
const classifiedChecks = classifyFailingChecks(failingCheckNames, failingCheckConclusions);
|
|
253
255
|
return this.buildFetchedPR({
|
|
254
256
|
id: ghPR.id,
|
|
255
257
|
url: prUrl,
|
|
@@ -293,7 +295,7 @@ export class PRMonitor {
|
|
|
293
295
|
* Determine the overall status of a PR
|
|
294
296
|
*/
|
|
295
297
|
determineStatus(input) {
|
|
296
|
-
const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate, lastMaintainerCommentDate, latestChangesRequestedDate, } = input;
|
|
298
|
+
const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
|
|
297
299
|
// Priority order: needs_response/needs_changes/changes_addressed > failing_ci > merge_conflict > incomplete_checklist > dormant > approaching_dormant > waiting_on_maintainer > waiting/healthy
|
|
298
300
|
if (hasUnrespondedComment) {
|
|
299
301
|
// If the contributor pushed a commit after the maintainer's comment,
|
|
@@ -304,8 +306,10 @@ export class PRMonitor {
|
|
|
304
306
|
if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
|
|
305
307
|
return 'needs_response';
|
|
306
308
|
}
|
|
307
|
-
if (ciStatus === 'failing')
|
|
309
|
+
if (ciStatus === 'failing' && hasActionableCIFailure)
|
|
308
310
|
return 'failing_ci';
|
|
311
|
+
// Non-actionable CI failures (infrastructure, fork, auth) don't block changes_addressed —
|
|
312
|
+
// the contributor can't fix them, so the relevant status is "waiting for re-review" (#502)
|
|
309
313
|
return 'changes_addressed';
|
|
310
314
|
}
|
|
311
315
|
return 'needs_response';
|
|
@@ -317,12 +321,13 @@ export class PRMonitor {
|
|
|
317
321
|
return 'needs_changes';
|
|
318
322
|
}
|
|
319
323
|
// Commit is after review — changes have been addressed
|
|
320
|
-
if (ciStatus === 'failing')
|
|
324
|
+
if (ciStatus === 'failing' && hasActionableCIFailure)
|
|
321
325
|
return 'failing_ci';
|
|
326
|
+
// Non-actionable CI failures don't block changes_addressed (#502)
|
|
322
327
|
return 'changes_addressed';
|
|
323
328
|
}
|
|
324
329
|
if (ciStatus === 'failing') {
|
|
325
|
-
return 'failing_ci';
|
|
330
|
+
return hasActionableCIFailure ? 'failing_ci' : 'ci_blocked';
|
|
326
331
|
}
|
|
327
332
|
if (hasMergeConflict) {
|
|
328
333
|
return 'merge_conflict';
|
|
@@ -67,7 +67,17 @@ export function isAllSelfReplies(reviewId, reviewComments) {
|
|
|
67
67
|
return false; // New thread, not a reply
|
|
68
68
|
const parentAuthor = authorMap.get(comment.in_reply_to_id);
|
|
69
69
|
const commentAuthor = comment.user?.login?.toLowerCase();
|
|
70
|
-
|
|
70
|
+
if (parentAuthor == null || commentAuthor == null || parentAuthor !== commentAuthor)
|
|
71
|
+
return false;
|
|
72
|
+
// A self-reply containing a question mark is likely a follow-up question
|
|
73
|
+
// directed at the PR author, not an informational addendum (#498).
|
|
74
|
+
// Null/empty body on a self-reply is anomalous — surface it rather than
|
|
75
|
+
// silently filtering, since the safe direction is to notify.
|
|
76
|
+
if (!comment.body)
|
|
77
|
+
return false;
|
|
78
|
+
if (comment.body.includes('?'))
|
|
79
|
+
return false;
|
|
80
|
+
return true;
|
|
71
81
|
});
|
|
72
82
|
}
|
|
73
83
|
/**
|
package/dist/core/state.js
CHANGED
|
@@ -13,6 +13,8 @@ const MODULE = 'state';
|
|
|
13
13
|
const CURRENT_STATE_VERSION = 2;
|
|
14
14
|
// Maximum number of events to retain in the event log
|
|
15
15
|
const MAX_EVENTS = 1000;
|
|
16
|
+
/** Repo scores older than this are considered stale and excluded from low-scoring lists. */
|
|
17
|
+
const SCORE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
16
18
|
// Lock file timeout: if a lock is older than this, it is considered stale
|
|
17
19
|
const LOCK_TIMEOUT_MS = 30_000; // 30 seconds
|
|
18
20
|
// Legacy path for migration
|
|
@@ -1026,8 +1028,19 @@ export class StateManager {
|
|
|
1026
1028
|
*/
|
|
1027
1029
|
getLowScoringRepos(maxScore) {
|
|
1028
1030
|
const threshold = maxScore ?? this.state.config.minRepoScoreThreshold;
|
|
1031
|
+
const now = Date.now();
|
|
1029
1032
|
return Object.values(this.state.repoScores)
|
|
1030
|
-
.filter((rs) =>
|
|
1033
|
+
.filter((rs) => {
|
|
1034
|
+
if (rs.score > threshold)
|
|
1035
|
+
return false;
|
|
1036
|
+
// Stale scores (>30 days) should not permanently block repos (#487)
|
|
1037
|
+
const age = now - new Date(rs.lastEvaluatedAt).getTime();
|
|
1038
|
+
if (!Number.isFinite(age)) {
|
|
1039
|
+
warn(MODULE, `Invalid lastEvaluatedAt for repo ${rs.repo}: "${rs.lastEvaluatedAt}", treating as stale`);
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
return age <= SCORE_TTL_MS;
|
|
1043
|
+
})
|
|
1031
1044
|
.sort((a, b) => a.score - b.score)
|
|
1032
1045
|
.map((rs) => rs.repo);
|
|
1033
1046
|
}
|
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
* fields we only update one place. Every factory accepts a `Partial<T>`
|
|
6
6
|
* override bag — callers only specify the fields relevant to their test.
|
|
7
7
|
*/
|
|
8
|
-
import type { FetchedPR, DailyDigest
|
|
8
|
+
import type { FetchedPR, DailyDigest } from './types.js';
|
|
9
9
|
import type { CapacityAssessment } from '../formatters/json.js';
|
|
10
10
|
export declare function makeFetchedPR(overrides?: Partial<FetchedPR>): FetchedPR;
|
|
11
11
|
export declare function makeDailyDigest(overrides?: Partial<DailyDigest>): DailyDigest;
|
|
12
|
-
export declare function makeShelvedPRRef(overrides?: Partial<ShelvedPRRef>): ShelvedPRRef;
|
|
13
12
|
export declare function makeCapacityAssessment(overrides?: Partial<CapacityAssessment>): CapacityAssessment;
|
|
14
|
-
export declare function makeAgentState(overrides?: Partial<AgentState>): AgentState;
|
package/dist/core/test-utils.js
CHANGED
|
@@ -69,20 +69,6 @@ export function makeDailyDigest(overrides = {}) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
|
-
// ShelvedPRRef
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
export function makeShelvedPRRef(overrides = {}) {
|
|
75
|
-
return {
|
|
76
|
-
number: 1,
|
|
77
|
-
url: 'https://github.com/owner/repo/pull/1',
|
|
78
|
-
title: 'Shelved PR',
|
|
79
|
-
repo: 'owner/repo',
|
|
80
|
-
daysSinceActivity: 45,
|
|
81
|
-
status: 'healthy',
|
|
82
|
-
...overrides,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
72
|
// CapacityAssessment
|
|
87
73
|
// ---------------------------------------------------------------------------
|
|
88
74
|
export function makeCapacityAssessment(overrides = {}) {
|
|
@@ -96,30 +82,3 @@ export function makeCapacityAssessment(overrides = {}) {
|
|
|
96
82
|
...overrides,
|
|
97
83
|
};
|
|
98
84
|
}
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// AgentState (partial — for tests that need a state object)
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
export function makeAgentState(overrides = {}) {
|
|
103
|
-
return {
|
|
104
|
-
version: 2,
|
|
105
|
-
repoScores: {},
|
|
106
|
-
config: {
|
|
107
|
-
setupComplete: false,
|
|
108
|
-
githubUsername: 'testuser',
|
|
109
|
-
excludeRepos: [],
|
|
110
|
-
maxActivePRs: 10,
|
|
111
|
-
dormantThresholdDays: 30,
|
|
112
|
-
approachingDormantDays: 25,
|
|
113
|
-
maxIssueAgeDays: 90,
|
|
114
|
-
languages: [],
|
|
115
|
-
labels: [],
|
|
116
|
-
trustedProjects: [],
|
|
117
|
-
minRepoScoreThreshold: 4,
|
|
118
|
-
starredRepos: [],
|
|
119
|
-
},
|
|
120
|
-
events: [],
|
|
121
|
-
lastRunAt: '2025-06-20T00:00:00Z',
|
|
122
|
-
activeIssues: [],
|
|
123
|
-
...overrides,
|
|
124
|
-
};
|
|
125
|
-
}
|