@oss-autopilot/core 0.47.2 → 0.48.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.
- package/dist/cli.bundle.cjs +47 -47
- package/dist/cli.bundle.cjs.map +3 -3
- package/dist/commands/dashboard-data.d.ts +8 -2
- package/dist/commands/dashboard-data.js +53 -9
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-process.js +1 -1
- package/dist/commands/dashboard-server.js +15 -13
- package/dist/core/github-stats.d.ts +10 -1
- package/dist/core/github-stats.js +58 -0
- package/dist/core/state.d.ts +17 -1
- package/dist/core/state.js +37 -3
- package/dist/core/types.d.ts +10 -2
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
|
-
import { type DailyDigest, type AgentState, type MergedPR, type StoredMergedPR, type CommentedIssue } from '../core/types.js';
|
|
6
|
+
import { type DailyDigest, type AgentState, type ClosedPR, type MergedPR, type StoredMergedPR, type StoredClosedPR, type CommentedIssue } from '../core/types.js';
|
|
7
7
|
export interface DashboardStats {
|
|
8
8
|
activePRs: number;
|
|
9
9
|
shelvedPRs: number;
|
|
@@ -11,7 +11,7 @@ export interface DashboardStats {
|
|
|
11
11
|
closedPRs: number;
|
|
12
12
|
mergeRate: string;
|
|
13
13
|
}
|
|
14
|
-
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number): DashboardStats;
|
|
14
|
+
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number, storedClosedCount?: number): DashboardStats;
|
|
15
15
|
/**
|
|
16
16
|
* Merge fresh API counts into existing stored counts.
|
|
17
17
|
* Months present in the fresh data are updated; months only in the existing data are preserved.
|
|
@@ -32,6 +32,7 @@ export interface DashboardFetchResult {
|
|
|
32
32
|
digest: DailyDigest;
|
|
33
33
|
commentedIssues: CommentedIssue[];
|
|
34
34
|
allMergedPRs: MergedPR[];
|
|
35
|
+
allClosedPRs: ClosedPR[];
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* Fetch fresh dashboard data from GitHub.
|
|
@@ -44,6 +45,11 @@ export declare function fetchDashboardData(token: string): Promise<DashboardFetc
|
|
|
44
45
|
* Skips entries with unparseable URLs.
|
|
45
46
|
*/
|
|
46
47
|
export declare function storedToMergedPRs(stored: StoredMergedPR[]): MergedPR[];
|
|
48
|
+
/**
|
|
49
|
+
* Convert StoredClosedPR[] to ClosedPR[] by deriving repo and number from URL.
|
|
50
|
+
* Skips entries with unparseable URLs.
|
|
51
|
+
*/
|
|
52
|
+
export declare function storedToClosedPRs(stored: StoredClosedPR[]): ClosedPR[];
|
|
47
53
|
/**
|
|
48
54
|
* Compute PRs grouped by repository from a digest and state.
|
|
49
55
|
* Used for chart data in the dashboard API.
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit } from '../core/index.js';
|
|
7
7
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
|
-
import { emptyPRCountsResult, fetchMergedPRsSince } from '../core/github-stats.js';
|
|
9
|
+
import { emptyPRCountsResult, fetchMergedPRsSince, fetchClosedPRsSince } from '../core/github-stats.js';
|
|
10
10
|
import { parseGitHubUrl } from '../core/utils.js';
|
|
11
11
|
import { isBelowMinStars, } from '../core/types.js';
|
|
12
12
|
import { toShelvedPRRef, buildStarFilter } from './daily.js';
|
|
13
13
|
const MODULE = 'dashboard-data';
|
|
14
|
-
export function buildDashboardStats(digest, state, storedMergedCount) {
|
|
14
|
+
export function buildDashboardStats(digest, state, storedMergedCount, storedClosedCount) {
|
|
15
15
|
const summary = digest.summary || {
|
|
16
16
|
totalActivePRs: 0,
|
|
17
17
|
totalMergedAllTime: 0,
|
|
@@ -19,16 +19,19 @@ export function buildDashboardStats(digest, state, storedMergedCount) {
|
|
|
19
19
|
totalNeedingAttention: 0,
|
|
20
20
|
};
|
|
21
21
|
const minStars = state.config.minStars ?? 50;
|
|
22
|
-
//
|
|
23
|
-
// when stored list hasn't caught up yet (
|
|
22
|
+
// Merged: use the higher of stored count vs repoScores aggregate to avoid regressions
|
|
23
|
+
// when the stored list hasn't caught up yet (initial fetch caps at 300 PRs)
|
|
24
24
|
const mergedPRs = storedMergedCount !== undefined
|
|
25
25
|
? Math.max(storedMergedCount, summary.totalMergedAllTime)
|
|
26
26
|
: summary.totalMergedAllTime;
|
|
27
|
+
// Closed: same anti-regression strategy — use the higher of stored count vs repoScores aggregate
|
|
28
|
+
const aggregateClosedCount = Object.values(state.repoScores || {}).reduce((sum, s) => sum + (isBelowMinStars(s.stargazersCount, minStars) ? 0 : s.closedWithoutMergeCount || 0), 0);
|
|
29
|
+
const closedPRs = storedClosedCount !== undefined ? Math.max(storedClosedCount, aggregateClosedCount) : aggregateClosedCount;
|
|
27
30
|
return {
|
|
28
31
|
activePRs: summary.totalActivePRs,
|
|
29
32
|
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
30
33
|
mergedPRs,
|
|
31
|
-
closedPRs
|
|
34
|
+
closedPRs,
|
|
32
35
|
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
33
36
|
};
|
|
34
37
|
}
|
|
@@ -102,9 +105,10 @@ export async function fetchDashboardData(token) {
|
|
|
102
105
|
const config = stateManager.getState().config;
|
|
103
106
|
// Build star filter from cached repoScores (#576)
|
|
104
107
|
const starFilter = buildStarFilter(stateManager.getState());
|
|
105
|
-
// Get
|
|
108
|
+
// Get watermarks for incremental PR fetch
|
|
106
109
|
const watermark = stateManager.getMergedPRWatermark();
|
|
107
|
-
const
|
|
110
|
+
const closedWatermark = stateManager.getClosedPRWatermark();
|
|
111
|
+
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues, newMergedPRs, newClosedPRs,] = await Promise.all([
|
|
108
112
|
prMonitor.fetchUserOpenPRs(),
|
|
109
113
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
110
114
|
if (isRateLimitOrAuthError(err))
|
|
@@ -151,6 +155,12 @@ export async function fetchDashboardData(token) {
|
|
|
151
155
|
warn(MODULE, `Failed to fetch merged PRs for storage: ${errorMessage(err)}`);
|
|
152
156
|
return [];
|
|
153
157
|
}),
|
|
158
|
+
fetchClosedPRsSince(octokit, config, closedWatermark).catch((err) => {
|
|
159
|
+
if (isRateLimitOrAuthError(err))
|
|
160
|
+
throw err;
|
|
161
|
+
warn(MODULE, `Failed to fetch closed PRs for storage: ${errorMessage(err)}`);
|
|
162
|
+
return [];
|
|
163
|
+
}),
|
|
154
164
|
]);
|
|
155
165
|
const commentedIssues = fetchedIssues.issues;
|
|
156
166
|
if (fetchedIssues.failures.length > 0) {
|
|
@@ -166,8 +176,16 @@ export async function fetchDashboardData(token) {
|
|
|
166
176
|
catch (error) {
|
|
167
177
|
warn(MODULE, `Failed to store merged PRs: ${errorMessage(error)}`);
|
|
168
178
|
}
|
|
169
|
-
//
|
|
179
|
+
// Store new closed PRs incrementally (dedupes by URL)
|
|
180
|
+
try {
|
|
181
|
+
stateManager.addClosedPRs(newClosedPRs);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
warn(MODULE, `Failed to store closed PRs: ${errorMessage(error)}`);
|
|
185
|
+
}
|
|
186
|
+
// Convert stored PRs to full types (derive repo/number from URL)
|
|
170
187
|
const allMergedPRs = storedToMergedPRs(stateManager.getMergedPRs());
|
|
188
|
+
const allClosedPRs = storedToClosedPRs(stateManager.getClosedPRs());
|
|
171
189
|
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
172
190
|
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
173
191
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
@@ -188,7 +206,7 @@ export async function fetchDashboardData(token) {
|
|
|
188
206
|
warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
|
|
189
207
|
}
|
|
190
208
|
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
191
|
-
return { digest, commentedIssues, allMergedPRs };
|
|
209
|
+
return { digest, commentedIssues, allMergedPRs, allClosedPRs };
|
|
192
210
|
}
|
|
193
211
|
/**
|
|
194
212
|
* Convert StoredMergedPR[] to MergedPR[] by deriving repo and number from URL.
|
|
@@ -216,6 +234,32 @@ export function storedToMergedPRs(stored) {
|
|
|
216
234
|
}
|
|
217
235
|
return results;
|
|
218
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Convert StoredClosedPR[] to ClosedPR[] by deriving repo and number from URL.
|
|
239
|
+
* Skips entries with unparseable URLs.
|
|
240
|
+
*/
|
|
241
|
+
export function storedToClosedPRs(stored) {
|
|
242
|
+
const results = [];
|
|
243
|
+
let skipped = 0;
|
|
244
|
+
for (const pr of stored) {
|
|
245
|
+
const parsed = parseGitHubUrl(pr.url);
|
|
246
|
+
if (!parsed) {
|
|
247
|
+
skipped++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
results.push({
|
|
251
|
+
url: pr.url,
|
|
252
|
+
repo: `${parsed.owner}/${parsed.repo}`,
|
|
253
|
+
number: parsed.number,
|
|
254
|
+
title: pr.title,
|
|
255
|
+
closedAt: pr.closedAt,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (skipped > 0) {
|
|
259
|
+
warn(MODULE, `Skipped ${skipped} stored closed PR(s) with unparseable URLs`);
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
219
263
|
/**
|
|
220
264
|
* Compute PRs grouped by repository from a digest and state.
|
|
221
265
|
* Used for chart data in the dashboard API.
|
|
@@ -102,7 +102,7 @@ export async function launchDashboardServer(options) {
|
|
|
102
102
|
if (info) {
|
|
103
103
|
// PID file appeared — verify the server is responding
|
|
104
104
|
if (await isDashboardServerRunning(info.port)) {
|
|
105
|
-
return { url: `http://localhost:${info.port}`, port: info.port, alreadyRunning: false };
|
|
105
|
+
return { url: `http://oss.localhost:${info.port}`, port: info.port, alreadyRunning: false };
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -85,7 +85,7 @@ export async function findRunningDashboardServer() {
|
|
|
85
85
|
}
|
|
86
86
|
// Process exists — verify it's actually our server via HTTP probe
|
|
87
87
|
if (await isDashboardServerRunning(info.port)) {
|
|
88
|
-
return { port: info.port, url: `http://localhost:${info.port}` };
|
|
88
|
+
return { port: info.port, url: `http://oss.localhost:${info.port}` };
|
|
89
89
|
}
|
|
90
90
|
// Process exists but not responding on expected port — stale
|
|
91
91
|
removeDashboardServerInfo();
|
|
@@ -12,7 +12,7 @@ import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js
|
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
13
|
import { warn } from '../core/logger.js';
|
|
14
14
|
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
|
|
15
|
-
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, } from './dashboard-data.js';
|
|
15
|
+
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, storedToClosedPRs, } from './dashboard-data.js';
|
|
16
16
|
import { openInBrowser } from './startup.js';
|
|
17
17
|
import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
|
|
18
18
|
import { RateLimiter } from './rate-limiter.js';
|
|
@@ -71,19 +71,20 @@ function applyStatusOverrides(prs, state) {
|
|
|
71
71
|
/**
|
|
72
72
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
73
73
|
*/
|
|
74
|
-
function buildDashboardJson(digest, state, commentedIssues, allMergedPRs) {
|
|
74
|
+
function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClosedPRs) {
|
|
75
75
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
76
76
|
const topRepos = computeTopRepos(prsByRepo);
|
|
77
77
|
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
78
|
-
// Derive
|
|
78
|
+
// Derive from state if not provided (e.g. initial load from cached state)
|
|
79
79
|
const mergedPRs = allMergedPRs ?? storedToMergedPRs(getStateManager().getMergedPRs());
|
|
80
|
-
|
|
80
|
+
const closedPRs = allClosedPRs ?? storedToClosedPRs(getStateManager().getClosedPRs());
|
|
81
|
+
// Filter out PRs from repos below the minStars threshold
|
|
81
82
|
const minStars = state.config.minStars ?? 50;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const stats = buildDashboardStats(digest, state, filteredMergedPRs.length);
|
|
83
|
+
const repoScores = state.repoScores || {};
|
|
84
|
+
const isAboveMinStars = (pr) => !isBelowMinStars(repoScores[pr.repo]?.stargazersCount, minStars);
|
|
85
|
+
const filteredMergedPRs = mergedPRs.filter(isAboveMinStars);
|
|
86
|
+
const filteredClosedPRs = closedPRs.filter(isAboveMinStars);
|
|
87
|
+
const stats = buildDashboardStats(digest, state, filteredMergedPRs.length, filteredClosedPRs.length);
|
|
87
88
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
88
89
|
return {
|
|
89
90
|
stats,
|
|
@@ -100,6 +101,7 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs) {
|
|
|
100
101
|
commentedIssues,
|
|
101
102
|
issueResponses,
|
|
102
103
|
allMergedPRs: filteredMergedPRs,
|
|
104
|
+
allClosedPRs: filteredClosedPRs,
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
/**
|
|
@@ -339,7 +341,7 @@ export async function startDashboardServer(options) {
|
|
|
339
341
|
const result = await fetchDashboardData(currentToken);
|
|
340
342
|
cachedDigest = result.digest;
|
|
341
343
|
cachedCommentedIssues = result.commentedIssues;
|
|
342
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs);
|
|
344
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
|
343
345
|
sendJson(res, 200, cachedJsonData);
|
|
344
346
|
}
|
|
345
347
|
catch (error) {
|
|
@@ -442,8 +444,8 @@ export async function startDashboardServer(options) {
|
|
|
442
444
|
startedAt: new Date().toISOString(),
|
|
443
445
|
version: getCLIVersion(),
|
|
444
446
|
});
|
|
445
|
-
const serverUrl = `http://localhost:${actualPort}`;
|
|
446
|
-
warn(MODULE, `Dashboard server running at ${serverUrl} (also: http://
|
|
447
|
+
const serverUrl = `http://oss.localhost:${actualPort}`;
|
|
448
|
+
warn(MODULE, `Dashboard server running at ${serverUrl} (also: http://localhost:${actualPort})`);
|
|
447
449
|
// ── Background refresh ─────────────────────────────────────────────────
|
|
448
450
|
// Port is bound and PID file written — now fetch fresh data from GitHub
|
|
449
451
|
// so subsequent /api/data requests get live data instead of cached state.
|
|
@@ -452,7 +454,7 @@ export async function startDashboardServer(options) {
|
|
|
452
454
|
.then((result) => {
|
|
453
455
|
cachedDigest = result.digest;
|
|
454
456
|
cachedCommentedIssues = result.commentedIssues;
|
|
455
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs);
|
|
457
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
|
456
458
|
warn(MODULE, 'Background data refresh complete');
|
|
457
459
|
})
|
|
458
460
|
.catch((error) => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
|
-
import { ClosedPR, MergedPR, StoredMergedPR, type StarFilter } from './types.js';
|
|
6
|
+
import { ClosedPR, MergedPR, StoredMergedPR, StoredClosedPR, type StarFilter } from './types.js';
|
|
7
7
|
/** TTL for cached PR count results (24 hours — these stats change slowly). */
|
|
8
8
|
export declare const PR_COUNTS_CACHE_TTL_MS: number;
|
|
9
9
|
/** Return type shared by both merged and closed PR count functions. */
|
|
@@ -50,3 +50,12 @@ export declare function fetchRecentlyMergedPRs(octokit: Octokit, config: {
|
|
|
50
50
|
export declare function fetchMergedPRsSince(octokit: Octokit, config: {
|
|
51
51
|
githubUsername: string;
|
|
52
52
|
}, since?: string): Promise<StoredMergedPR[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Fetch closed-without-merge PRs since a watermark date for incremental storage.
|
|
55
|
+
* If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
|
|
56
|
+
* Returns StoredClosedPR[] (minimal: url, title, closedAt) for state persistence.
|
|
57
|
+
* Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
|
|
58
|
+
*/
|
|
59
|
+
export declare function fetchClosedPRsSince(octokit: Octokit, config: {
|
|
60
|
+
githubUsername: string;
|
|
61
|
+
}, since?: string): Promise<StoredClosedPR[]>;
|
|
@@ -287,3 +287,61 @@ export async function fetchMergedPRsSince(octokit, config, since) {
|
|
|
287
287
|
debug(MODULE, `Fetched ${results.length} merged PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
288
288
|
return results;
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Fetch closed-without-merge PRs since a watermark date for incremental storage.
|
|
292
|
+
* If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
|
|
293
|
+
* Returns StoredClosedPR[] (minimal: url, title, closedAt) for state persistence.
|
|
294
|
+
* Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
|
|
295
|
+
*/
|
|
296
|
+
export async function fetchClosedPRsSince(octokit, config, since) {
|
|
297
|
+
if (!config.githubUsername) {
|
|
298
|
+
warn(MODULE, 'Skipping closed PRs fetch: no githubUsername configured.');
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const dateFilter = since ? ` closed:>${since}` : '';
|
|
302
|
+
const q = `is:pr is:closed is:unmerged author:${config.githubUsername} -user:${config.githubUsername}${dateFilter}`;
|
|
303
|
+
debug(MODULE, `Fetching closed PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
304
|
+
const results = [];
|
|
305
|
+
let page = 1;
|
|
306
|
+
let fetched = 0;
|
|
307
|
+
let totalCount;
|
|
308
|
+
while (true) {
|
|
309
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
310
|
+
q,
|
|
311
|
+
sort: 'updated',
|
|
312
|
+
order: 'desc',
|
|
313
|
+
per_page: 100,
|
|
314
|
+
page,
|
|
315
|
+
});
|
|
316
|
+
totalCount = data.total_count;
|
|
317
|
+
for (const item of data.items) {
|
|
318
|
+
const parsed = parseGitHubUrl(item.html_url);
|
|
319
|
+
if (!parsed) {
|
|
320
|
+
warn(MODULE, `Skipping closed PR with unparseable URL: ${item.html_url}`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
324
|
+
continue;
|
|
325
|
+
const closedAt = item.closed_at || '';
|
|
326
|
+
if (!closedAt) {
|
|
327
|
+
warn(MODULE, `Skipping closed PR with no close date: ${item.html_url}`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
results.push({
|
|
331
|
+
url: item.html_url,
|
|
332
|
+
title: item.title,
|
|
333
|
+
closedAt,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
fetched += data.items.length;
|
|
337
|
+
if (fetched >= totalCount || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
page++;
|
|
341
|
+
}
|
|
342
|
+
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
343
|
+
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} closed PRs. Oldest PRs may be missing.`);
|
|
344
|
+
}
|
|
345
|
+
debug(MODULE, `Fetched ${results.length} closed PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
346
|
+
return results;
|
|
347
|
+
}
|
package/dist/core/state.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* State management for the OSS Contribution Agent
|
|
3
3
|
* Persists state to a JSON file in ~/.oss-autopilot/
|
|
4
4
|
*/
|
|
5
|
-
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo, StatusOverride, FetchedPRStatus, StoredMergedPR } from './types.js';
|
|
5
|
+
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } from './types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Acquire an advisory file lock using exclusive-create (`wx` flag).
|
|
8
8
|
* If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
|
|
@@ -129,6 +129,22 @@ export declare class StateManager {
|
|
|
129
129
|
* @returns ISO date string of the most recent merge, or undefined if no stored PRs.
|
|
130
130
|
*/
|
|
131
131
|
getMergedPRWatermark(): string | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Get all stored closed PRs.
|
|
134
|
+
* @returns Array of stored closed PRs, sorted by closedAt desc.
|
|
135
|
+
*/
|
|
136
|
+
getClosedPRs(): StoredClosedPR[];
|
|
137
|
+
/**
|
|
138
|
+
* Add new closed PRs to the stored list. Deduplicates by URL and sorts by closedAt desc.
|
|
139
|
+
* @param prs - New closed PRs to add.
|
|
140
|
+
*/
|
|
141
|
+
addClosedPRs(prs: StoredClosedPR[]): void;
|
|
142
|
+
/**
|
|
143
|
+
* Get the most recent closedAt timestamp from stored closed PRs.
|
|
144
|
+
* Used as the watermark for incremental fetching.
|
|
145
|
+
* @returns ISO date string of the most recent close, or undefined if no stored PRs.
|
|
146
|
+
*/
|
|
147
|
+
getClosedPRWatermark(): string | undefined;
|
|
132
148
|
/**
|
|
133
149
|
* Store cached local repo scan results (#84).
|
|
134
150
|
* @param cache - The scan results, paths scanned, and timestamp.
|
package/dist/core/state.js
CHANGED
|
@@ -576,10 +576,44 @@ export class StateManager {
|
|
|
576
576
|
if (!prs || prs.length === 0)
|
|
577
577
|
return undefined;
|
|
578
578
|
// List is sorted desc by mergedAt, so first element is most recent
|
|
579
|
-
|
|
580
|
-
|
|
579
|
+
return prs[0].mergedAt || undefined;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get all stored closed PRs.
|
|
583
|
+
* @returns Array of stored closed PRs, sorted by closedAt desc.
|
|
584
|
+
*/
|
|
585
|
+
getClosedPRs() {
|
|
586
|
+
return this.state.closedPRs ?? [];
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Add new closed PRs to the stored list. Deduplicates by URL and sorts by closedAt desc.
|
|
590
|
+
* @param prs - New closed PRs to add.
|
|
591
|
+
*/
|
|
592
|
+
addClosedPRs(prs) {
|
|
593
|
+
if (prs.length === 0)
|
|
594
|
+
return;
|
|
595
|
+
if (!this.state.closedPRs) {
|
|
596
|
+
this.state.closedPRs = [];
|
|
597
|
+
}
|
|
598
|
+
const existingUrls = new Set(this.state.closedPRs.map((pr) => pr.url));
|
|
599
|
+
const newPRs = prs.filter((pr) => !existingUrls.has(pr.url));
|
|
600
|
+
if (newPRs.length === 0)
|
|
601
|
+
return;
|
|
602
|
+
this.state.closedPRs.push(...newPRs);
|
|
603
|
+
this.state.closedPRs.sort((a, b) => b.closedAt.localeCompare(a.closedAt));
|
|
604
|
+
debug(MODULE, `Added ${newPRs.length} closed PRs (total: ${this.state.closedPRs.length})`);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Get the most recent closedAt timestamp from stored closed PRs.
|
|
608
|
+
* Used as the watermark for incremental fetching.
|
|
609
|
+
* @returns ISO date string of the most recent close, or undefined if no stored PRs.
|
|
610
|
+
*/
|
|
611
|
+
getClosedPRWatermark() {
|
|
612
|
+
const prs = this.state.closedPRs;
|
|
613
|
+
if (!prs || prs.length === 0)
|
|
581
614
|
return undefined;
|
|
582
|
-
|
|
615
|
+
// List is sorted desc by closedAt, so first element is most recent
|
|
616
|
+
return prs[0].closedAt || undefined;
|
|
583
617
|
}
|
|
584
618
|
/**
|
|
585
619
|
* Store cached local repo scan results (#84).
|
package/dist/core/types.d.ts
CHANGED
|
@@ -298,7 +298,7 @@ export interface StateEvent {
|
|
|
298
298
|
/** Event-specific payload (e.g., `{ repo: "owner/repo", number: 42 }` for PR events). */
|
|
299
299
|
data: Record<string, unknown>;
|
|
300
300
|
}
|
|
301
|
-
/** Minimal record of a PR that was closed without being merged, used in the daily digest. */
|
|
301
|
+
/** Minimal record of a PR that was closed without being merged, used in the daily digest and dashboard detail view. */
|
|
302
302
|
export interface ClosedPR {
|
|
303
303
|
url: string;
|
|
304
304
|
repo: string;
|
|
@@ -313,7 +313,13 @@ export interface StoredMergedPR {
|
|
|
313
313
|
title: string;
|
|
314
314
|
mergedAt: string;
|
|
315
315
|
}
|
|
316
|
-
/** Minimal
|
|
316
|
+
/** Minimal closed PR data persisted in state.json. Repo/number derived from URL at display time. */
|
|
317
|
+
export interface StoredClosedPR {
|
|
318
|
+
url: string;
|
|
319
|
+
title: string;
|
|
320
|
+
closedAt: string;
|
|
321
|
+
}
|
|
322
|
+
/** Minimal record of a PR that was merged, used in the daily digest and dashboard detail view. */
|
|
317
323
|
export interface MergedPR {
|
|
318
324
|
url: string;
|
|
319
325
|
repo: string;
|
|
@@ -391,6 +397,8 @@ export interface AgentState {
|
|
|
391
397
|
localRepoCache?: LocalRepoCache;
|
|
392
398
|
/** All merged PRs stored incrementally. Source of truth for the merged PR detail view. */
|
|
393
399
|
mergedPRs?: StoredMergedPR[];
|
|
400
|
+
/** All closed PRs stored incrementally. Source of truth for the closed PR detail view. */
|
|
401
|
+
closedPRs?: StoredClosedPR[];
|
|
394
402
|
activeIssues: TrackedIssue[];
|
|
395
403
|
}
|
|
396
404
|
/** Cached results from scanning the filesystem for local git clones (#84). */
|