@oss-autopilot/core 0.43.1 → 0.44.1
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 +20 -0
- package/dist/cli-registry.js +935 -0
- package/dist/cli.bundle.cjs +125 -126
- package/dist/cli.bundle.cjs.map +7 -0
- package/dist/cli.d.ts +5 -6
- package/dist/cli.js +14 -826
- package/dist/commands/dashboard-data.d.ts +10 -7
- package/dist/commands/dashboard-data.js +36 -3
- package/dist/commands/dashboard-formatters.d.ts +2 -10
- package/dist/commands/dashboard-formatters.js +2 -16
- package/dist/commands/dashboard-lifecycle.d.ts +1 -1
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-scripts.d.ts +1 -1
- package/dist/commands/dashboard-server.js +50 -51
- package/dist/commands/dashboard-templates.d.ts +3 -2
- package/dist/commands/dashboard-templates.js +3 -2
- package/dist/commands/dashboard.d.ts +5 -9
- package/dist/commands/dashboard.js +7 -111
- package/dist/commands/startup.d.ts +3 -2
- package/dist/commands/startup.js +33 -57
- package/dist/core/daily-logic.js +1 -1
- package/dist/core/display-utils.js +8 -1
- package/dist/core/http-cache.d.ts +10 -0
- package/dist/core/http-cache.js +19 -0
- 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/types.d.ts +4 -2
- package/dist/core/utils.d.ts +3 -6
- package/dist/core/utils.js +3 -6
- package/dist/formatters/json.d.ts +3 -2
- package/package.json +3 -3
package/dist/commands/startup.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Startup command
|
|
3
3
|
* Combines all pre-flight checks into a single CLI invocation:
|
|
4
|
-
* auth check, setup check, daily fetch, dashboard
|
|
4
|
+
* auth check, setup check, daily fetch, dashboard launch, version detection, issue list detection.
|
|
5
5
|
*
|
|
6
6
|
* Replaces the ~100-line inline bash script in commands/oss.md with a single
|
|
7
7
|
* `node cli.bundle.cjs startup --json` call, reducing UI noise in Claude Code.
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { execFile } from 'child_process';
|
|
11
|
-
import { getStateManager, getGitHubToken, getCLIVersion
|
|
11
|
+
import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
|
|
12
12
|
import { errorMessage } from '../core/errors.js';
|
|
13
|
-
import { warn } from '../core/logger.js';
|
|
14
13
|
import { executeDailyCheck } from './daily.js';
|
|
15
|
-
import { writeDashboardFromState } from './dashboard.js';
|
|
16
14
|
import { launchDashboardServer } from './dashboard-lifecycle.js';
|
|
15
|
+
import { writeDashboardFromState } from './dashboard.js';
|
|
17
16
|
/**
|
|
18
17
|
* Parse issueListPath from a config file's YAML frontmatter.
|
|
19
18
|
* Returns the path string or undefined if not found.
|
|
@@ -94,21 +93,21 @@ export function detectIssueList() {
|
|
|
94
93
|
return { path: issueListPath, source, availableCount: 0, completedCount: 0 };
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
|
-
function openInBrowser(
|
|
96
|
+
export function openInBrowser(url) {
|
|
98
97
|
let openCmd;
|
|
99
98
|
let args;
|
|
100
99
|
switch (process.platform) {
|
|
101
100
|
case 'darwin':
|
|
102
101
|
openCmd = 'open';
|
|
103
|
-
args = [
|
|
102
|
+
args = [url];
|
|
104
103
|
break;
|
|
105
104
|
case 'win32':
|
|
106
105
|
openCmd = 'cmd';
|
|
107
|
-
args = ['/c', 'start', '',
|
|
106
|
+
args = ['/c', 'start', '', url];
|
|
108
107
|
break;
|
|
109
108
|
default:
|
|
110
109
|
openCmd = 'xdg-open';
|
|
111
|
-
args = [
|
|
110
|
+
args = [url];
|
|
112
111
|
break;
|
|
113
112
|
}
|
|
114
113
|
execFile(openCmd, args, (error) => {
|
|
@@ -117,31 +116,12 @@ function openInBrowser(filePath) {
|
|
|
117
116
|
}
|
|
118
117
|
});
|
|
119
118
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Check whether the dashboard HTML file is at least as recent as state.json.
|
|
122
|
-
* Returns true when the dashboard exists and its mtime >= state mtime,
|
|
123
|
-
* meaning there is no need to regenerate it.
|
|
124
|
-
*/
|
|
125
|
-
function isDashboardFresh() {
|
|
126
|
-
try {
|
|
127
|
-
const dashPath = getDashboardPath();
|
|
128
|
-
if (!fs.existsSync(dashPath))
|
|
129
|
-
return false;
|
|
130
|
-
const dashMtime = fs.statSync(dashPath).mtimeMs;
|
|
131
|
-
const stateMtime = fs.statSync(getStatePath()).mtimeMs;
|
|
132
|
-
return dashMtime >= stateMtime;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
warn('startup', `Failed to check dashboard freshness, will regenerate: ${errorMessage(error)}`);
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
119
|
/**
|
|
140
120
|
* Run startup checks and return structured output.
|
|
141
121
|
* Returns StartupOutput with one of three shapes:
|
|
142
122
|
* 1. Setup incomplete: { version, setupComplete: false }
|
|
143
123
|
* 2. Auth failure: { version, setupComplete: true, authError: "..." }
|
|
144
|
-
* 3. Success: { version, setupComplete: true, daily,
|
|
124
|
+
* 3. Success: { version, setupComplete: true, daily, dashboardUrl?, dashboardPath?, issueList? }
|
|
145
125
|
*
|
|
146
126
|
* Errors from the daily check propagate to the caller.
|
|
147
127
|
*/
|
|
@@ -163,56 +143,52 @@ export async function runStartup() {
|
|
|
163
143
|
}
|
|
164
144
|
// 3. Run daily check
|
|
165
145
|
const daily = await executeDailyCheck(token);
|
|
166
|
-
// 4.
|
|
167
|
-
//
|
|
146
|
+
// 4. Launch interactive SPA dashboard (with static HTML fallback)
|
|
147
|
+
// Skip opening on first run (0 PRs) — the welcome flow handles onboarding
|
|
148
|
+
let dashboardUrl;
|
|
168
149
|
let dashboardPath;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.error('[STARTUP] Dashboard HTML is fresh, skipping regeneration');
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
150
|
+
let dashboardOpened = false;
|
|
151
|
+
function tryStaticHtmlFallback() {
|
|
152
|
+
try {
|
|
175
153
|
dashboardPath = writeDashboardFromState();
|
|
154
|
+
openInBrowser(dashboardPath);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch (htmlError) {
|
|
158
|
+
console.error('[STARTUP] Static HTML dashboard fallback also failed:', errorMessage(htmlError));
|
|
159
|
+
return false;
|
|
176
160
|
}
|
|
177
161
|
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
console.error('[STARTUP] Dashboard generation failed:', errorMessage(error));
|
|
180
|
-
}
|
|
181
|
-
// 5. Launch interactive SPA dashboard (preferred) with static HTML fallback
|
|
182
|
-
// Skip opening on first run (0 PRs) — the welcome flow handles onboarding
|
|
183
|
-
let dashboardUrl;
|
|
184
|
-
let dashboardOpened = false;
|
|
185
162
|
if (daily.digest.summary.totalActivePRs > 0) {
|
|
186
|
-
let spaResult = null;
|
|
187
163
|
try {
|
|
188
|
-
spaResult = await launchDashboardServer();
|
|
164
|
+
const spaResult = await launchDashboardServer();
|
|
165
|
+
if (spaResult) {
|
|
166
|
+
dashboardUrl = spaResult.url;
|
|
167
|
+
openInBrowser(spaResult.url);
|
|
168
|
+
dashboardOpened = true;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.error('[STARTUP] Dashboard SPA assets not found, falling back to static HTML dashboard');
|
|
172
|
+
dashboardOpened = tryStaticHtmlFallback();
|
|
173
|
+
}
|
|
189
174
|
}
|
|
190
175
|
catch (error) {
|
|
191
176
|
console.error('[STARTUP] SPA dashboard launch failed:', errorMessage(error));
|
|
192
|
-
|
|
193
|
-
if (spaResult) {
|
|
194
|
-
dashboardUrl = spaResult.url;
|
|
195
|
-
openInBrowser(spaResult.url);
|
|
196
|
-
dashboardOpened = true;
|
|
197
|
-
}
|
|
198
|
-
else if (dashboardPath) {
|
|
199
|
-
// SPA unavailable (assets not built) — fall back to static HTML
|
|
200
|
-
openInBrowser(dashboardPath);
|
|
201
|
-
dashboardOpened = true;
|
|
177
|
+
dashboardOpened = tryStaticHtmlFallback();
|
|
202
178
|
}
|
|
203
179
|
}
|
|
204
180
|
// Append dashboard status to brief summary (only startup opens the browser, not daily)
|
|
205
181
|
if (dashboardOpened) {
|
|
206
182
|
daily.briefSummary += ' | Dashboard opened in browser';
|
|
207
183
|
}
|
|
208
|
-
//
|
|
184
|
+
// 5. Detect issue list
|
|
209
185
|
const issueList = detectIssueList();
|
|
210
186
|
return {
|
|
211
187
|
version,
|
|
212
188
|
setupComplete: true,
|
|
213
189
|
daily,
|
|
214
|
-
dashboardPath,
|
|
215
190
|
dashboardUrl,
|
|
191
|
+
dashboardPath,
|
|
216
192
|
issueList,
|
|
217
193
|
};
|
|
218
194
|
}
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -509,5 +509,5 @@ export function printDigest(digest, capacity, commentedIssues = []) {
|
|
|
509
509
|
console.log('');
|
|
510
510
|
}
|
|
511
511
|
console.log('Run with --json for structured output');
|
|
512
|
-
console.log('Run "dashboard
|
|
512
|
+
console.log('Run "dashboard serve" for browser view');
|
|
513
513
|
}
|
|
@@ -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]',
|
|
@@ -101,3 +101,13 @@ export declare function cachedRequest<T>(cache: HttpCache, url: string, fetcher:
|
|
|
101
101
|
data: T;
|
|
102
102
|
headers?: Record<string, string>;
|
|
103
103
|
}>): Promise<T>;
|
|
104
|
+
/**
|
|
105
|
+
* Time-based cache wrapper (no ETag / conditional requests).
|
|
106
|
+
*
|
|
107
|
+
* If a cached result exists and is younger than `maxAgeMs`, returns it.
|
|
108
|
+
* Otherwise calls `fetcher`, caches the result, and returns it.
|
|
109
|
+
*
|
|
110
|
+
* Use this for expensive operations whose results change slowly
|
|
111
|
+
* (e.g. search queries, project health checks).
|
|
112
|
+
*/
|
|
113
|
+
export declare function cachedTimeBased<T>(cache: HttpCache, key: string, maxAgeMs: number, fetcher: () => Promise<T>): Promise<T>;
|
package/dist/core/http-cache.js
CHANGED
|
@@ -272,6 +272,25 @@ export async function cachedRequest(cache, url, fetcher) {
|
|
|
272
272
|
cleanup();
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Time-based cache wrapper (no ETag / conditional requests).
|
|
277
|
+
*
|
|
278
|
+
* If a cached result exists and is younger than `maxAgeMs`, returns it.
|
|
279
|
+
* Otherwise calls `fetcher`, caches the result, and returns it.
|
|
280
|
+
*
|
|
281
|
+
* Use this for expensive operations whose results change slowly
|
|
282
|
+
* (e.g. search queries, project health checks).
|
|
283
|
+
*/
|
|
284
|
+
export async function cachedTimeBased(cache, key, maxAgeMs, fetcher) {
|
|
285
|
+
const cached = cache.getIfFresh(key, maxAgeMs);
|
|
286
|
+
if (cached) {
|
|
287
|
+
debug(MODULE, `Time-based cache hit for ${key}`);
|
|
288
|
+
return cached;
|
|
289
|
+
}
|
|
290
|
+
const result = await fetcher();
|
|
291
|
+
cache.set(key, '', result);
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
275
294
|
/**
|
|
276
295
|
* Detect whether an error is a 304 Not Modified response.
|
|
277
296
|
* Octokit throws a RequestError with status 304 for conditional requests.
|
|
@@ -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
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface DetermineStatusInput {
|
|
|
54
54
|
latestCommitDate?: string;
|
|
55
55
|
lastMaintainerCommentDate?: string;
|
|
56
56
|
latestChangesRequestedDate?: string;
|
|
57
|
+
/** True if at least one failing CI check is classified as 'actionable'. */
|
|
58
|
+
hasActionableCIFailure?: boolean;
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
59
61
|
* Computed status for a {@link FetchedPR}, determined by `PRMonitor.determineStatus()`.
|
|
@@ -62,8 +64,7 @@ export interface DetermineStatusInput {
|
|
|
62
64
|
* **Action required (contributor must act):**
|
|
63
65
|
* - `needs_response` — Maintainer commented after the contributor's last activity
|
|
64
66
|
* - `needs_changes` — Reviewer requested changes (via review, not just a comment)
|
|
65
|
-
* - `failing_ci` — One or more CI checks are failing
|
|
66
|
-
* - `ci_blocked` — CI cannot run (e.g., first-time contributor approval needed) *(reserved)*
|
|
67
|
+
* - `failing_ci` — One or more CI checks are failing (at least one is actionable)
|
|
67
68
|
* - `ci_not_running` — No CI checks have been triggered *(reserved)*
|
|
68
69
|
* - `merge_conflict` — PR has merge conflicts with the base branch
|
|
69
70
|
* - `needs_rebase` — PR branch is significantly behind upstream *(reserved)*
|
|
@@ -71,6 +72,7 @@ export interface DetermineStatusInput {
|
|
|
71
72
|
* - `incomplete_checklist` — PR body has unchecked required checkboxes
|
|
72
73
|
*
|
|
73
74
|
* **Waiting (no action needed right now):**
|
|
75
|
+
* - `ci_blocked` — All failing CI checks are non-actionable (infrastructure, fork limitation, auth gate)
|
|
74
76
|
* - `changes_addressed` — Contributor pushed commits after reviewer feedback; awaiting re-review
|
|
75
77
|
* - `waiting` — CI is pending or no specific action needed
|
|
76
78
|
* - `waiting_on_maintainer` — PR is approved and CI passes; waiting for maintainer to merge
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Returns the oss-autopilot data directory path, creating it if it does not exist.
|
|
6
6
|
*
|
|
7
7
|
* The directory is located at `~/.oss-autopilot/` and serves as the root for
|
|
8
|
-
* all persisted user data (state, backups,
|
|
8
|
+
* all persisted user data (state, backups, cache).
|
|
9
9
|
*
|
|
10
10
|
* @returns Absolute path to the data directory (e.g., `/Users/you/.oss-autopilot`)
|
|
11
11
|
*
|
|
@@ -53,11 +53,8 @@ export declare function getBackupDir(): string;
|
|
|
53
53
|
*/
|
|
54
54
|
export declare function getCacheDir(): string;
|
|
55
55
|
/**
|
|
56
|
-
* Returns the path to the
|
|
57
|
-
*
|
|
58
|
-
* Implicitly creates the data directory via {@link getDataDir} if it does not exist.
|
|
59
|
-
*
|
|
60
|
-
* @returns Absolute path to `dashboard.html`
|
|
56
|
+
* Returns the path to the static HTML dashboard file (~/.oss-autopilot/dashboard.html).
|
|
57
|
+
* Used as a fallback when the interactive SPA dashboard cannot be launched.
|
|
61
58
|
*
|
|
62
59
|
* @example
|
|
63
60
|
* const dashPath = getDashboardPath();
|
package/dist/core/utils.js
CHANGED
|
@@ -15,7 +15,7 @@ let tokenFetchAttempted = false;
|
|
|
15
15
|
* Returns the oss-autopilot data directory path, creating it if it does not exist.
|
|
16
16
|
*
|
|
17
17
|
* The directory is located at `~/.oss-autopilot/` and serves as the root for
|
|
18
|
-
* all persisted user data (state, backups,
|
|
18
|
+
* all persisted user data (state, backups, cache).
|
|
19
19
|
*
|
|
20
20
|
* @returns Absolute path to the data directory (e.g., `/Users/you/.oss-autopilot`)
|
|
21
21
|
*
|
|
@@ -83,11 +83,8 @@ export function getCacheDir() {
|
|
|
83
83
|
return dir;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Returns the path to the
|
|
87
|
-
*
|
|
88
|
-
* Implicitly creates the data directory via {@link getDataDir} if it does not exist.
|
|
89
|
-
*
|
|
90
|
-
* @returns Absolute path to `dashboard.html`
|
|
86
|
+
* Returns the path to the static HTML dashboard file (~/.oss-autopilot/dashboard.html).
|
|
87
|
+
* Used as a fallback when the interactive SPA dashboard cannot be launched.
|
|
91
88
|
*
|
|
92
89
|
* @example
|
|
93
90
|
* const dashPath = getDashboardPath();
|
|
@@ -200,16 +200,17 @@ export interface IssueListInfo {
|
|
|
200
200
|
* Three valid shapes:
|
|
201
201
|
* 1. Setup incomplete: { version, setupComplete: false }
|
|
202
202
|
* 2. Auth failure: { version, setupComplete: true, authError: "..." }
|
|
203
|
-
* 3. Success: { version, setupComplete: true, daily,
|
|
203
|
+
* 3. Success: { version, setupComplete: true, daily, dashboardUrl?, issueList? }
|
|
204
204
|
*/
|
|
205
205
|
export interface StartupOutput {
|
|
206
206
|
version: string;
|
|
207
207
|
setupComplete: boolean;
|
|
208
208
|
authError?: string;
|
|
209
209
|
daily?: DailyOutput;
|
|
210
|
-
dashboardPath?: string;
|
|
211
210
|
/** URL of the interactive SPA dashboard server, when running (e.g., "http://localhost:3000") */
|
|
212
211
|
dashboardUrl?: string;
|
|
212
|
+
/** Path to the static HTML dashboard file (fallback when SPA cannot launch) */
|
|
213
|
+
dashboardPath?: string;
|
|
213
214
|
issueList?: IssueListInfo;
|
|
214
215
|
}
|
|
215
216
|
/** A single parsed issue from a markdown list (#82) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.1",
|
|
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": "^
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
55
|
"@vitest/coverage-v8": "^4.0.18",
|
|
56
56
|
"esbuild": "^0.27.3",
|
|
57
57
|
"tsx": "^4.21.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc",
|
|
63
|
-
"bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --minify --outfile=dist/cli.bundle.cjs",
|
|
63
|
+
"bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --minify --sourcemap --outfile=dist/cli.bundle.cjs",
|
|
64
64
|
"start": "tsx src/cli.ts",
|
|
65
65
|
"dev": "tsx watch src/cli.ts",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|